From 2f4a3d2e5da2a45723089e1e53e5edb07b58b812 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 28 Feb 2018 02:30:20 -0800 Subject: [PATCH] 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 --- RELEASENOTES.md | 15 +- .../exoplayer2/castdemo/PlayerManager.java | 16 +- .../ext/mediasession/TimelineQueueEditor.java | 38 +- .../google/android/exoplayer2/ExoPlayer.java | 4 +- .../source/ConcatenatingMediaSource.java | 849 +++++++++++++--- .../DynamicConcatenatingMediaSource.java | 811 +-------------- .../source/ConcatenatingMediaSourceTest.java | 957 ++++++++++++++---- .../DynamicConcatenatingMediaSourceTest.java | 871 ---------------- 8 files changed, 1512 insertions(+), 2049 deletions(-) delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e93badfe70..4498093717 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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 ### diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index ac488ff3fd..70c4831bc5 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -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 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. diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java index 65090a3c1c..048bd70640 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java @@ -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}. - *

- * This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles + * A {@link MediaSessionConnector.QueueEditor} implementation based on the {@link + * ConcatenatingMediaSource}. + * + *

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; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index c13fd6cacd..39a6243933 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -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}). *

  • {@link Renderer}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 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index ee6600b098..b37b6249b5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -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 { +public class ConcatenatingMediaSource extends CompositeMediaSource + implements PlayerMessage.Target { - private final MediaSource[] mediaSources; - private final Timeline[] timelines; - private final Object[] manifests; - private final Map 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 mediaSourcesPublic; + + // Accessed on the playback thread. + private final List mediaSourceHolders; + private final MediaSourceHolder query; + private final Map mediaSourceByMediaPeriod; + private final List deferredMediaPeriods; + private final List 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 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 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 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 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. + * + *

    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. + * + *

    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 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 addMessage = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, 1); + addMediaSourceInternal(addMessage.index, addMessage.customData); + scheduleListenerNotification(addMessage.actionOnCompletion); + break; + case MSG_ADD_MULTIPLE: + MessageData> addMultipleMessage = + (MessageData>) message; + shuffleOrder = + shuffleOrder.cloneAndInsert( + addMultipleMessage.index, addMultipleMessage.customData.size()); + addMediaSourcesInternal(addMultipleMessage.index, addMultipleMessage.customData); + scheduleListenerNotification(addMultipleMessage.actionOnCompletion); + break; + case MSG_REMOVE: + MessageData removeMessage = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndRemove(removeMessage.index); + removeMediaSourceInternal(removeMessage.index); + scheduleListenerNotification(removeMessage.actionOnCompletion); + break; + case MSG_MOVE: + MessageData moveMessage = (MessageData) 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 actionsOnCompletion = ((List) 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 actionsOnCompletion = + pendingOnCompletionActions.isEmpty() + ? Collections.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 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 { + + 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 { + + 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 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 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; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 317166a75e..37313fd1ab 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -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 - 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 mediaSourcesPublic; - - // Accessed on the playback thread. - private final List mediaSourceHolders; - private final MediaSourceHolder query; - private final Map mediaSourceByMediaPeriod; - private final List deferredMediaPeriods; - private final List 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 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 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 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 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. - * - *

    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. - * - *

    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 addMessage = (MessageData) message; - shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, 1); - addMediaSourceInternal(addMessage.index, addMessage.customData); - scheduleListenerNotification(addMessage.actionOnCompletion); - break; - case MSG_ADD_MULTIPLE: - MessageData> addMultipleMessage = - (MessageData>) message; - shuffleOrder = - shuffleOrder.cloneAndInsert( - addMultipleMessage.index, addMultipleMessage.customData.size()); - addMediaSourcesInternal(addMultipleMessage.index, addMultipleMessage.customData); - scheduleListenerNotification(addMultipleMessage.actionOnCompletion); - break; - case MSG_REMOVE: - MessageData removeMessage = (MessageData) message; - shuffleOrder = shuffleOrder.cloneAndRemove(removeMessage.index); - removeMediaSourceInternal(removeMessage.index); - scheduleListenerNotification(removeMessage.actionOnCompletion); - break; - case MSG_MOVE: - MessageData moveMessage = (MessageData) 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 actionsOnCompletion = ((List) 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 actionsOnCompletion = - pendingOnCompletionActions.isEmpty() - ? Collections.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 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 { - - 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 { - - 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 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); } } - diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 257966f5c3..038ff5505e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,15 @@ package com.google.android.exoplayer2.source; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.verify; +import android.os.ConditionVariable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; @@ -29,8 +33,13 @@ import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @@ -39,142 +48,292 @@ import org.robolectric.annotation.Config; @Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) public final class ConcatenatingMediaSourceTest { - @Test - public void testEmptyConcatenation() throws IOException { - for (boolean atomic : new boolean[] {false, true}) { - Timeline timeline = getConcatenatedTimeline(atomic); - TimelineAsserts.assertEmpty(timeline); + private ConcatenatingMediaSource mediaSource; + private MediaSourceTestRunner testRunner; - timeline = getConcatenatedTimeline(atomic, Timeline.EMPTY); - TimelineAsserts.assertEmpty(timeline); + @Before + public void setUp() throws Exception { + mediaSource = new ConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); + testRunner = new MediaSourceTestRunner(mediaSource, null); + } - timeline = getConcatenatedTimeline(atomic, Timeline.EMPTY, Timeline.EMPTY, Timeline.EMPTY); - TimelineAsserts.assertEmpty(timeline); - } + @After + public void tearDown() throws Exception { + testRunner.release(); } @Test - public void testSingleMediaSource() throws IOException { - Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111)); + public void testPlaylistChangesAfterPreparation() throws IOException, InterruptedException { + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertEmpty(timeline); + + FakeMediaSource[] childSources = createMediaSources(7); + + // Add first source. + mediaSource.addMediaSource(childSources[0]); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1); TimelineAsserts.assertWindowIds(timeline, 111); - TimelineAsserts.assertPeriodCounts(timeline, 3); - for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); + + // Add at front of queue. + mediaSource.addMediaSource(0, childSources[1]); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 1); + TimelineAsserts.assertWindowIds(timeline, 222, 111); + + // Add at back of queue. + mediaSource.addMediaSource(childSources[2]); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 111, 333); + + // Add in the middle. + mediaSource.addMediaSource(1, childSources[3]); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 333); + + // Add bulk. + mediaSource.addMediaSources( + 3, Arrays.asList(childSources[4], childSources[5], childSources[6])); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); + + // Move sources. + mediaSource.moveMediaSource(2, 3); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 5, 1, 6, 7, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 444, 555, 111, 666, 777, 333); + mediaSource.moveMediaSource(3, 2); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); + mediaSource.moveMediaSource(0, 6); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 4, 1, 5, 6, 7, 3, 2); + TimelineAsserts.assertWindowIds(timeline, 444, 111, 555, 666, 777, 333, 222); + mediaSource.moveMediaSource(6, 0); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); + + // Remove in the middle. + mediaSource.removeMediaSource(3); + testRunner.assertTimelineChangeBlocking(); + mediaSource.removeMediaSource(3); + testRunner.assertTimelineChangeBlocking(); + mediaSource.removeMediaSource(3); + testRunner.assertTimelineChangeBlocking(); + mediaSource.removeMediaSource(1); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 111, 333); + for (int i = 3; i <= 6; i++) { + childSources[i].assertReleased(); } - timeline = getConcatenatedTimeline(true, createFakeTimeline(3, 111)); - TimelineAsserts.assertWindowIds(timeline, 111); - TimelineAsserts.assertPeriodCounts(timeline, 3); - for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); - } - } - - @Test - public void testMultipleMediaSources() throws IOException { - Timeline[] timelines = { - createFakeTimeline(3, 111), createFakeTimeline(1, 222), createFakeTimeline(3, 333) - }; - Timeline timeline = getConcatenatedTimeline(false, timelines); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); - TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + // Assert correct next and previous indices behavior after some insertions and removals. TimelineAsserts.assertNextWindowIndices( timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); + timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0); + assertThat(timeline.getLastWindowIndex(false)).isEqualTo(timeline.getWindowCount() - 1); TimelineAsserts.assertNextWindowIndices( timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); - assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0); - assertThat(timeline.getLastWindowIndex(false)).isEqualTo(2); - assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); + assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(timeline.getWindowCount() - 1); assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0); - timeline = getConcatenatedTimeline(true, timelines); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); - TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); - for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ONE, shuffled, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ALL, shuffled, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); - assertThat(timeline.getFirstWindowIndex(shuffled)).isEqualTo(0); - assertThat(timeline.getLastWindowIndex(shuffled)).isEqualTo(2); + // 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 testNestedMediaSources() throws IOException { - Timeline timeline = - getConcatenatedTimeline( - false, - getConcatenatedTimeline(false, createFakeTimeline(1, 111), createFakeTimeline(1, 222)), - getConcatenatedTimeline(true, createFakeTimeline(1, 333), createFakeTimeline(1, 444))); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444); - TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 3, 2); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ALL, false, 3, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, 1, 2, 3, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 3, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 3, 0); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, 1, 3, C.INDEX_UNSET, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 3, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 3, 0, 2); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 3, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 3, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 3, 1); + public void testPlaylistWithLazyMediaSource() throws IOException, InterruptedException { + // Create some normal (immediately preparing) sources and some lazy sources whose timeline + // updates need to be triggered. + FakeMediaSource[] fastSources = createMediaSources(2); + final FakeMediaSource[] lazySources = new FakeMediaSource[4]; + for (int i = 0; i < 4; i++) { + lazySources[i] = new FakeMediaSource(null, null); + } + + // Add lazy sources and normal sources before preparation. Also remove one lazy source again + // before preparation to check it doesn't throw or change the result. + mediaSource.addMediaSource(lazySources[0]); + mediaSource.addMediaSource(0, fastSources[0]); + mediaSource.removeMediaSource(1); + mediaSource.addMediaSource(1, lazySources[1]); + testRunner.assertNoTimelineChange(); + + // Prepare and assert that the timeline contains all information for normal sources while having + // placeholder information for lazy sources. + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1); + TimelineAsserts.assertWindowIds(timeline, 111, null); + TimelineAsserts.assertWindowIsDynamic(timeline, false, true); + + // Trigger source info refresh for lazy source and check that the timeline now contains all + // information for all windows. + testRunner.runOnPlaybackThread( + new Runnable() { + @Override + public void run() { + lazySources[1].setNewSourceInfo(createFakeTimeline(8), null); + } + }); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 9); + TimelineAsserts.assertWindowIds(timeline, 111, 999); + TimelineAsserts.assertWindowIsDynamic(timeline, false, false); + testRunner.assertPrepareAndReleaseAllPeriods(); + + // Add further lazy and normal sources after preparation. Also remove one lazy source again to + // check it doesn't throw or change the result. + mediaSource.addMediaSource(1, lazySources[2]); + testRunner.assertTimelineChangeBlocking(); + mediaSource.addMediaSource(2, fastSources[1]); + testRunner.assertTimelineChangeBlocking(); + mediaSource.addMediaSource(0, lazySources[3]); + testRunner.assertTimelineChangeBlocking(); + mediaSource.removeMediaSource(2); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 2, 9); + TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999); + TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false); + + // Create a period from an unprepared lazy media source and assert Callback.onPrepared is not + // called yet. + MediaPeriod lazyPeriod = + testRunner.createPeriod( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + CountDownLatch preparedCondition = testRunner.preparePeriod(lazyPeriod, 0); + assertThat(preparedCondition.getCount()).isEqualTo(1); + + // Assert that a second period can also be created and released without problems. + MediaPeriod secondLazyPeriod = + testRunner.createPeriod( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + testRunner.releasePeriod(secondLazyPeriod); + + // Trigger source info refresh for lazy media source. Assert that now all information is + // available again and the previously created period now also finished preparing. + testRunner.runOnPlaybackThread( + new Runnable() { + @Override + public void run() { + lazySources[3].setNewSourceInfo(createFakeTimeline(7), null); + } + }); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9); + TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999); + TimelineAsserts.assertWindowIsDynamic(timeline, false, false, false, false); + assertThat(preparedCondition.getCount()).isEqualTo(0); + + // Release the period and source. + testRunner.releasePeriod(lazyPeriod); + testRunner.releaseSource(); + + // Assert all sources were fully released. + for (FakeMediaSource fastSource : fastSources) { + fastSource.assertReleased(); + } + for (FakeMediaSource lazySource : lazySources) { + lazySource.assertReleased(); + } } @Test - public void testEmptyTimelineMediaSources() throws IOException { - // Empty timelines in the front, back, and the middle (single and multiple in a row). - Timeline[] timelines = { - Timeline.EMPTY, - createFakeTimeline(1, 111), - Timeline.EMPTY, - Timeline.EMPTY, - createFakeTimeline(2, 222), - Timeline.EMPTY, - createFakeTimeline(3, 333), - Timeline.EMPTY - }; - Timeline timeline = getConcatenatedTimeline(false, timelines); + public void testEmptyTimelineMediaSource() throws IOException, InterruptedException { + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertEmpty(timeline); + + mediaSource.addMediaSource(new FakeMediaSource(Timeline.EMPTY, null)); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertEmpty(timeline); + + mediaSource.addMediaSources( + Arrays.asList( + new MediaSource[] { + new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), + new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), + new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null) + })); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertEmpty(timeline); + + // Insert non-empty media source to leave empty sources at the start, the end, and the middle + // (with single and multiple empty sources in a row). + MediaSource[] mediaSources = createMediaSources(3); + mediaSource.addMediaSource(1, mediaSources[0]); + testRunner.assertTimelineChangeBlocking(); + mediaSource.addMediaSource(4, mediaSources[1]); + testRunner.assertTimelineChangeBlocking(); + mediaSource.addMediaSource(6, mediaSources[2]); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); TimelineAsserts.assertPreviousWindowIndices( @@ -197,29 +356,263 @@ public final class ConcatenatingMediaSourceTest { assertThat(timeline.getLastWindowIndex(false)).isEqualTo(2); assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(2); assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0); + testRunner.assertPrepareAndReleaseAllPeriods(); + } - timeline = getConcatenatedTimeline(true, timelines); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); - TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); - for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ONE, shuffled, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ALL, shuffled, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); - assertThat(timeline.getFirstWindowIndex(shuffled)).isEqualTo(0); - assertThat(timeline.getLastWindowIndex(shuffled)).isEqualTo(2); + @Test + public void testDynamicChangeOfEmptyTimelines() throws IOException { + FakeMediaSource[] childSources = + new FakeMediaSource[] { + new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null), + new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null), + new FakeMediaSource(Timeline.EMPTY, /* manifest= */ null), + }; + Timeline nonEmptyTimeline = new FakeTimeline(/* windowCount = */ 1); + + mediaSource.addMediaSources(Arrays.asList(childSources)); + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertEmpty(timeline); + + childSources[0].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1); + + childSources[2].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1); + + childSources[1].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); + } + + @Test + public void testIllegalArguments() { + MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null); + + // Null sources. + try { + mediaSource.addMediaSource(null); + fail("Null mediaSource not allowed."); + } catch (NullPointerException e) { + // Expected. + } + + MediaSource[] mediaSources = {validSource, null}; + try { + mediaSource.addMediaSources(Arrays.asList(mediaSources)); + fail("Null mediaSource not allowed."); + } catch (NullPointerException e) { + // Expected. + } + } + + @Test + public void testCustomCallbackBeforePreparationAddSingle() { + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSource(createFakeMediaSource(), runnable); + verify(runnable).run(); + } + + @Test + public void testCustomCallbackBeforePreparationAddMultiple() { + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSources( + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + runnable); + verify(runnable).run(); + } + + @Test + public void testCustomCallbackBeforePreparationAddSingleWithIndex() { + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), runnable); + verify(runnable).run(); + } + + @Test + public void testCustomCallbackBeforePreparationAddMultipleWithIndex() { + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSources( + /* index */ 0, + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + runnable); + verify(runnable).run(); + } + + @Test + public void testCustomCallbackBeforePreparationRemove() { + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSource(createFakeMediaSource()); + mediaSource.removeMediaSource(/* index */ 0, runnable); + verify(runnable).run(); + } + + @Test + public void testCustomCallbackBeforePreparationMove() { + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSources( + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); + mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, runnable); + verify(runnable).run(); + } + + @Test + public void testCustomCallbackAfterPreparationAddSingle() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(createFakeMediaSource(), timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getWindowCount()).isEqualTo(1); + } finally { + dummyMainThread.release(); + } + } + + @Test + public void testCustomCallbackAfterPreparationAddMultiple() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSources( + Arrays.asList( + new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getWindowCount()).isEqualTo(2); + } finally { + dummyMainThread.release(); + } + } + + @Test + public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getWindowCount()).isEqualTo(1); + } finally { + dummyMainThread.release(); + } + } + + @Test + public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSources( + /* index */ 0, + Arrays.asList( + new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getWindowCount()).isEqualTo(2); + } finally { + dummyMainThread.release(); + } + } + + @Test + public void testCustomCallbackAfterPreparationRemove() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(createFakeMediaSource()); + } + }); + testRunner.assertTimelineChangeBlocking(); + + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.removeMediaSource(/* index */ 0, timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getWindowCount()).isEqualTo(0); + } finally { + dummyMainThread.release(); + } + } + + @Test + public void testCustomCallbackAfterPreparationMove() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSources( + Arrays.asList( + new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); + } + }); + testRunner.assertTimelineChangeBlocking(); + + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getWindowCount()).isEqualTo(2); + } finally { + dummyMainThread.release(); } } @Test public void testPeriodCreationWithAds() throws IOException, InterruptedException { - // Create media source with ad child source. + // Create concatenated media source with ad child source. Timeline timelineContentOnly = new FakeTimeline( new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); @@ -235,125 +628,243 @@ public final class ConcatenatingMediaSourceTest { /* 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(); ConcatenatingMediaSource mediaSource = - new ConcatenatingMediaSource(mediaSourceContentOnly, mediaSourceWithAds); + new ConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0)); + testRunner = new MediaSourceTestRunner(mediaSource, null); + mediaSource.addMediaSources(Arrays.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); + } - MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); - try { - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); + @Test + public void testNestedTimeline() throws IOException { + ConcatenatingMediaSource nestedSource1 = + new ConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); + ConcatenatingMediaSource nestedSource2 = + new ConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0)); + mediaSource.addMediaSource(nestedSource1); + mediaSource.addMediaSource(nestedSource2); + testRunner.prepareSource(); + FakeMediaSource[] childSources = createMediaSources(4); + nestedSource1.addMediaSource(childSources[0]); + testRunner.assertTimelineChangeBlocking(); + nestedSource1.addMediaSource(childSources[1]); + testRunner.assertTimelineChangeBlocking(); + nestedSource2.addMediaSource(childSources[2]); + testRunner.assertTimelineChangeBlocking(); + nestedSource2.addMediaSource(childSources[3]); + Timeline timeline = testRunner.assertTimelineChangeBlocking(); - // 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)); - } finally { - testRunner.release(); - } + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444); + TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3, 4); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 3, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, 3, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 3, 0); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 3, C.INDEX_UNSET, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 3, 0, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 3, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 3, 1); + } + + @Test + public void testRemoveChildSourceWithActiveMediaPeriod() throws IOException { + FakeMediaSource childSource = createFakeMediaSource(); + mediaSource.addMediaSource(childSource); + testRunner.prepareSource(); + MediaPeriod mediaPeriod = + testRunner.createPeriod( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + mediaSource.removeMediaSource(/* index= */ 0); + testRunner.assertTimelineChangeBlocking(); + testRunner.releasePeriod(mediaPeriod); + childSource.assertReleased(); + testRunner.releaseSource(); } @Test public void testDuplicateMediaSources() throws IOException, InterruptedException { FakeMediaSource childSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2), /* manifest= */ null); - ConcatenatingMediaSource mediaSource = - new ConcatenatingMediaSource(childSource, childSource, childSource); - MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); - try { - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1); - 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= */ 1, /* windowSequenceNumber= */ 1), - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 3), - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 5)); + mediaSource.addMediaSource(childSource); + mediaSource.addMediaSource(childSource); + testRunner.prepareSource(); + mediaSource.addMediaSources(Arrays.asList(childSource, childSource)); + Timeline timeline = testRunner.assertTimelineChangeBlocking(); - testRunner.releaseSource(); - childSource.assertReleased(); - } finally { - testRunner.release(); - } + 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); - ConcatenatingMediaSource nestedConcatenation = - new ConcatenatingMediaSource(childSource, childSource); - ConcatenatingMediaSource mediaSource = - new ConcatenatingMediaSource(childSource, nestedConcatenation, nestedConcatenation); - MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); - try { - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1); + ConcatenatingMediaSource nestedConcatenation = new ConcatenatingMediaSource(); - testRunner.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.prepareSource(); + mediaSource.addMediaSources( + Arrays.asList(childSource, nestedConcatenation, nestedConcatenation)); + testRunner.assertTimelineChangeBlocking(); + nestedConcatenation.addMediaSource(childSource); + testRunner.assertTimelineChangeBlocking(); + nestedConcatenation.addMediaSource(childSource); + Timeline timeline = testRunner.assertTimelineChangeBlocking(); - testRunner.releaseSource(); - childSource.assertReleased(); - } finally { - testRunner.release(); - } + 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(); } - /** - * Wraps the specified timelines in a {@link ConcatenatingMediaSource} and returns the - * concatenated timeline. - */ - private static Timeline getConcatenatedTimeline(boolean isRepeatOneAtomic, Timeline... timelines) - throws IOException { - FakeMediaSource[] mediaSources = new FakeMediaSource[timelines.length]; - for (int i = 0; i < timelines.length; i++) { - mediaSources[i] = new FakeMediaSource(timelines[i], null); + private static FakeMediaSource[] createMediaSources(int count) { + FakeMediaSource[] sources = new FakeMediaSource[count]; + for (int i = 0; i < count; i++) { + sources[i] = new FakeMediaSource(createFakeTimeline(i), null); } - ConcatenatingMediaSource mediaSource = - new ConcatenatingMediaSource( - isRepeatOneAtomic, new FakeShuffleOrder(mediaSources.length), mediaSources); - MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); - try { - Timeline timeline = testRunner.prepareSource(); - testRunner.releaseSource(); - for (int i = 0; i < mediaSources.length; i++) { - mediaSources[i].assertReleased(); + 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; - } finally { - testRunner.release(); } } - - private static FakeTimeline createFakeTimeline(int periodCount, int windowId) { - return new FakeTimeline(new TimelineWindowDefinition(periodCount, windowId)); - } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java deleted file mode 100644 index c2da872789..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ /dev/null @@ -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.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.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.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.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.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; - } - } -}