diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index b71b54575b..b1ae541411 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -4,6 +4,9 @@
* 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)).
### 2.7.0 ###
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
index 1899c815da..1010a27178 100644
--- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
@@ -20,6 +20,7 @@ import android.support.annotation.Nullable;
import android.view.ViewGroup;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
@@ -33,10 +34,12 @@ import java.io.IOException;
* @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
*/
@Deprecated
-public final class ImaAdsMediaSource implements MediaSource {
+public final class ImaAdsMediaSource extends BaseMediaSource {
private final AdsMediaSource adsMediaSource;
+ private Listener adsMediaSourceListener;
+
/**
* Constructs a new source that inserts ads linearly with the content specified by
* {@code contentMediaSource}.
@@ -74,18 +77,16 @@ public final class ImaAdsMediaSource implements MediaSource {
}
@Override
- public void prepareSource(
- final ExoPlayer player, boolean isTopLevelSource, final Listener listener) {
- adsMediaSource.prepareSource(
- player,
- isTopLevelSource,
+ public void prepareSourceInternal(final ExoPlayer player, boolean isTopLevelSource) {
+ adsMediaSourceListener =
new Listener() {
@Override
public void onSourceInfoRefreshed(
MediaSource source, Timeline timeline, @Nullable Object manifest) {
- listener.onSourceInfoRefreshed(ImaAdsMediaSource.this, timeline, manifest);
+ refreshSourceInfo(timeline, manifest);
}
- });
+ };
+ adsMediaSource.prepareSource(player, isTopLevelSource, adsMediaSourceListener);
}
@Override
@@ -104,7 +105,7 @@ public final class ImaAdsMediaSource implements MediaSource {
}
@Override
- public void releaseSource() {
- adsMediaSource.releaseSource();
+ public void releaseSourceInternal() {
+ adsMediaSource.releaseSource(adsMediaSourceListener);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
index e05068a7b3..2272bef573 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -799,7 +799,7 @@ import java.util.Collections;
resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult);
if (releaseMediaSource) {
if (mediaSource != null) {
- mediaSource.releaseSource();
+ mediaSource.releaseSource(/* listener= */ this);
mediaSource = null;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java
new file mode 100644
index 0000000000..a6924b5e05
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2018 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 android.support.annotation.Nullable;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.ArrayList;
+
+/**
+ * Base {@link MediaSource} implementation to handle parallel reuse.
+ *
+ *
Whenever an implementing subclass needs to provide a new timeline and/or manifest, it must
+ * call {@link #refreshSourceInfo(Timeline, Object)} to notify all listeners.
+ */
+public abstract class BaseMediaSource implements MediaSource {
+
+ private final ArrayList sourceInfoListeners;
+
+ private ExoPlayer player;
+ private Timeline timeline;
+ private Object manifest;
+
+ public BaseMediaSource() {
+ sourceInfoListeners = new ArrayList<>(/* initialCapacity= */ 1);
+ }
+
+ /**
+ * Starts source preparation. This method is called at most once until the next call to {@link
+ * #releaseSourceInternal()}.
+ *
+ * @param player The player for which this source is being prepared.
+ * @param isTopLevelSource Whether this source has been passed directly to {@link
+ * ExoPlayer#prepare(MediaSource)} or {@link ExoPlayer#prepare(MediaSource, boolean,
+ * boolean)}.
+ */
+ protected abstract void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource);
+
+ /**
+ * Releases the source. This method is called exactly once after each call to {@link
+ * #prepareSourceInternal(ExoPlayer, boolean)}.
+ */
+ protected abstract void releaseSourceInternal();
+
+ /**
+ * Updates timeline and manifest and notifies all listeners of the update.
+ *
+ * @param timeline The new {@link Timeline}.
+ * @param manifest The new manifest. May be null.
+ */
+ protected final void refreshSourceInfo(Timeline timeline, @Nullable Object manifest) {
+ this.timeline = timeline;
+ this.manifest = manifest;
+ for (Listener listener : sourceInfoListeners) {
+ listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest);
+ }
+ }
+
+ @Override
+ public final void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ Assertions.checkArgument(this.player == null || this.player == player);
+ sourceInfoListeners.add(listener);
+ if (this.player == null) {
+ this.player = player;
+ prepareSourceInternal(player, isTopLevelSource);
+ } else if (timeline != null) {
+ listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest);
+ }
+ }
+
+ @Override
+ public final void releaseSource(Listener listener) {
+ sourceInfoListeners.remove(listener);
+ if (sourceInfoListeners.isEmpty()) {
+ player = null;
+ timeline = null;
+ manifest = null;
+ releaseSourceInternal();
+ }
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
index 9ff704e75a..e25795443e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
@@ -84,7 +84,6 @@ public final class ClippingMediaSource extends CompositeMediaSource {
private final boolean enableInitialDiscontinuity;
private final ArrayList mediaPeriods;
- private MediaSource.Listener sourceListener;
private IllegalClippingException clippingError;
/**
@@ -131,9 +130,8 @@ public final class ClippingMediaSource extends CompositeMediaSource {
}
@Override
- public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
- super.prepareSource(player, isTopLevelSource, listener);
- sourceListener = listener;
+ public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
+ super.prepareSourceInternal(player, isTopLevelSource);
prepareChildSource(/* id= */ null, mediaSource);
}
@@ -161,10 +159,9 @@ public final class ClippingMediaSource extends CompositeMediaSource {
}
@Override
- public void releaseSource() {
- super.releaseSource();
+ public void releaseSourceInternal() {
+ super.releaseSourceInternal();
clippingError = null;
- sourceListener = null;
}
@Override
@@ -180,7 +177,7 @@ public final class ClippingMediaSource extends CompositeMediaSource {
clippingError = e;
return;
}
- sourceListener.onSourceInfoRefreshed(this, clippingTimeline, manifest);
+ refreshSourceInfo(clippingTimeline, manifest);
int count = mediaPeriods.size();
for (int i = 0; i < count; i++) {
mediaPeriods.get(i).setClipping(startUs, endUs);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java
index 6472fe3c2f..4f5a3c7a4e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java
@@ -28,9 +28,9 @@ import java.util.HashMap;
*
* @param The type of the id used to identify prepared child sources.
*/
-public abstract class CompositeMediaSource implements MediaSource {
+public abstract class CompositeMediaSource extends BaseMediaSource {
- private final HashMap childSources;
+ private final HashMap childSources;
private ExoPlayer player;
/** Create composite media source without child sources. */
@@ -40,23 +40,23 @@ public abstract class CompositeMediaSource implements MediaSource {
@Override
@CallSuper
- public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
this.player = player;
}
@Override
@CallSuper
public void maybeThrowSourceInfoRefreshError() throws IOException {
- for (MediaSource childSource : childSources.values()) {
- childSource.maybeThrowSourceInfoRefreshError();
+ for (MediaSourceAndListener childSource : childSources.values()) {
+ childSource.mediaSource.maybeThrowSourceInfoRefreshError();
}
}
@Override
@CallSuper
- public void releaseSource() {
- for (MediaSource childSource : childSources.values()) {
- childSource.releaseSource();
+ public void releaseSourceInternal() {
+ for (MediaSourceAndListener childSource : childSources.values()) {
+ childSource.mediaSource.releaseSource(childSource.listener);
}
childSources.clear();
player = null;
@@ -81,24 +81,23 @@ public abstract class CompositeMediaSource implements MediaSource {
* this method.
*
* Any child sources that aren't explicitly released with {@link #releaseChildSource(Object)}
- * will be released in {@link #releaseSource()}.
+ * will be released in {@link #releaseSourceInternal()}.
*
* @param id A unique id to identify the child source preparation. Null is allowed as an id.
* @param mediaSource The child {@link MediaSource}.
*/
- protected void prepareChildSource(@Nullable final T id, final MediaSource mediaSource) {
+ protected final void prepareChildSource(@Nullable final T id, MediaSource mediaSource) {
Assertions.checkArgument(!childSources.containsKey(id));
- childSources.put(id, mediaSource);
- mediaSource.prepareSource(
- player,
- /* isTopLevelSource= */ false,
+ Listener sourceListener =
new Listener() {
@Override
public void onSourceInfoRefreshed(
MediaSource source, Timeline timeline, @Nullable Object manifest) {
- onChildSourceInfoRefreshed(id, mediaSource, timeline, manifest);
+ onChildSourceInfoRefreshed(id, source, timeline, manifest);
}
- });
+ };
+ childSources.put(id, new MediaSourceAndListener(mediaSource, sourceListener));
+ mediaSource.prepareSource(player, /* isTopLevelSource= */ false, sourceListener);
}
/**
@@ -106,8 +105,19 @@ public abstract class CompositeMediaSource implements MediaSource {
*
* @param id The unique id used to prepare the child source.
*/
- protected void releaseChildSource(@Nullable T id) {
- MediaSource removedChild = childSources.remove(id);
- removedChild.releaseSource();
+ protected final void releaseChildSource(@Nullable T id) {
+ MediaSourceAndListener removedChild = childSources.remove(id);
+ removedChild.mediaSource.releaseSource(removedChild.listener);
+ }
+
+ private static final class MediaSourceAndListener {
+
+ public final MediaSource mediaSource;
+ public final Listener listener;
+
+ public MediaSourceAndListener(MediaSource mediaSource, Listener listener) {
+ this.mediaSource = mediaSource;
+ this.listener = listener;
+ }
}
}
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 c29367e109..ee6600b098 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
@@ -40,7 +40,6 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource
implements PlayerMessage.Target {
@@ -63,7 +64,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<
private final boolean isAtomic;
private ExoPlayer player;
- private Listener listener;
private boolean listenerNotificationScheduled;
private ShuffleOrder shuffleOrder;
private int windowCount;
@@ -107,9 +107,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<
/**
* Appends a {@link MediaSource} to the playlist.
- *
- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same
- * piece of media multiple times, use a new instance each time.
*
* @param mediaSource The {@link MediaSource} to be added to the list.
*/
@@ -119,9 +116,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<
/**
* Appends a {@link MediaSource} to the playlist and executes a custom action on completion.
- *
- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same
- * piece of media multiple times, use a new instance each time.
*
* @param mediaSource The {@link MediaSource} to be added to the list.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media
@@ -134,9 +128,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<
/**
* Adds a {@link MediaSource} to the playlist.
- *
- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same
- * piece of media multiple times, use a new instance each time.
*
* @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()}.
@@ -148,9 +139,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<
/**
* Adds a {@link MediaSource} to the playlist and executes a custom action on completion.
- *
- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same
- * piece of media multiple times, use a new instance each time.
*
* @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()}.
@@ -161,7 +149,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<
public synchronized void addMediaSource(int index, MediaSource mediaSource,
@Nullable Runnable actionOnCompletion) {
Assertions.checkNotNull(mediaSource);
- Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource));
mediaSourcesPublic.add(index, mediaSource);
if (player != null) {
player
@@ -176,9 +163,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<
/**
* Appends multiple {@link MediaSource}s to the playlist.
- *
- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same
- * piece of media multiple times, use a new instance each time.
*
* @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.
@@ -190,9 +174,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<
/**
* Appends multiple {@link MediaSource}s to the playlist and executes a custom action on
* completion.
- *
- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same
- * piece of media multiple times, use a new instance each time.
*
* @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.
@@ -206,9 +187,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<
/**
* Adds multiple {@link MediaSource}s to the playlist.
- *
- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same
- * piece of media multiple times, use a new instance each time.
*
* @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()}.
@@ -221,9 +199,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<
/**
* Adds multiple {@link MediaSource}s to the playlist and executes a custom action on completion.
- *
- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same
- * piece of media multiple times, use a new instance each time.
*
* @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()}.
@@ -236,7 +211,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<
@Nullable Runnable actionOnCompletion) {
for (MediaSource mediaSource : mediaSources) {
Assertions.checkNotNull(mediaSource);
- Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource));
}
mediaSourcesPublic.addAll(index, mediaSources);
if (player != null && !mediaSources.isEmpty()) {
@@ -252,10 +226,9 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<
/**
* Removes a {@link MediaSource} from the playlist.
- *
- * Note: {@link MediaSource} instances are not designed to be re-used, and so the instance being
- * removed should not be re-added. If you want to move the instance use
- * {@link #moveMediaSource(int, int)} instead.
+ *
+ *
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()}.
@@ -266,10 +239,9 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<
/**
* Removes a {@link MediaSource} from the playlist and executes a custom action on completion.
- *
- * Note: {@link MediaSource} instances are not designed to be re-used, and so the instance being
- * removed should not be re-added. If you want to move the instance use
- * {@link #moveMediaSource(int, int)} instead.
+ *
+ *
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()}.
@@ -347,11 +319,9 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<
}
@Override
- public synchronized void prepareSource(ExoPlayer player, boolean isTopLevelSource,
- Listener listener) {
- super.prepareSource(player, isTopLevelSource, listener);
+ public synchronized void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
+ super.prepareSourceInternal(player, isTopLevelSource);
this.player = player;
- this.listener = listener;
shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size());
addMediaSourcesInternal(0, mediaSourcesPublic);
scheduleListenerNotification(/* actionOnCompletion= */ null);
@@ -391,11 +361,10 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<
}
@Override
- public void releaseSource() {
- super.releaseSource();
+ public void releaseSourceInternal() {
+ super.releaseSourceInternal();
mediaSourceHolders.clear();
player = null;
- listener = null;
shuffleOrder = shuffleOrder.cloneAndClear();
windowCount = 0;
periodCount = 0;
@@ -473,8 +442,7 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<
? Collections.emptyList()
: new ArrayList<>(pendingOnCompletionActions);
pendingOnCompletionActions.clear();
- listener.onSourceInfoRefreshed(
- this,
+ refreshSourceInfo(
new ConcatenatedTimeline(
mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic),
/* manifest= */ null);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java
index 14453653af..20ef5ab147 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java
@@ -35,16 +35,17 @@ import java.io.IOException;
/**
* Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}.
- *
- * If the possible input stream container formats are known, pass a factory that instantiates
- * extractors for them to the constructor. Otherwise, pass a {@link DefaultExtractorsFactory} to
- * use the default extractors. When reading a new stream, the first {@link Extractor} in the array
- * of extractors created by the factory that returns {@code true} from {@link Extractor#sniff} will
- * be used to extract samples from the input stream.
- *
- * Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking.
+ *
+ *
If the possible input stream container formats are known, pass a factory that instantiates
+ * extractors for them to the constructor. Otherwise, pass a {@link DefaultExtractorsFactory} to use
+ * the default extractors. When reading a new stream, the first {@link Extractor} in the array of
+ * extractors created by the factory that returns {@code true} from {@link Extractor#sniff} will be
+ * used to extract samples from the input stream.
+ *
+ *
Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking.
*/
-public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPeriod.Listener {
+public final class ExtractorMediaSource extends BaseMediaSource
+ implements ExtractorMediaPeriod.Listener {
/**
* Listener of {@link ExtractorMediaSource} events.
*
@@ -100,7 +101,6 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
private final String customCacheKey;
private final int continueLoadingCheckIntervalBytes;
- private MediaSource.Listener sourceListener;
private long timelineDurationUs;
private boolean timelineIsSeekable;
@@ -324,8 +324,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
}
@Override
- public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
- sourceListener = listener;
+ public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
notifySourceInfoRefreshed(C.TIME_UNSET, false);
}
@@ -355,8 +354,8 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
}
@Override
- public void releaseSource() {
- sourceListener = null;
+ public void releaseSourceInternal() {
+ // Do nothing.
}
// ExtractorMediaPeriod.Listener implementation.
@@ -378,8 +377,9 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
timelineDurationUs = durationUs;
timelineIsSeekable = isSeekable;
// TODO: Make timeline dynamic until its duration is known. This is non-trivial. See b/69703223.
- sourceListener.onSourceInfoRefreshed(this,
- new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, false), null);
+ refreshSourceInfo(
+ new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, /* isDynamic= */ false),
+ /* manifest= */ null);
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java
index e2ef4eb5fa..774074b016 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java
@@ -36,7 +36,6 @@ public final class LoopingMediaSource extends CompositeMediaSource {
private final int loopCount;
private int childPeriodCount;
- private Listener listener;
/**
* Loops the provided source indefinitely. Note that it is usually better to use
@@ -61,9 +60,8 @@ public final class LoopingMediaSource extends CompositeMediaSource {
}
@Override
- public void prepareSource(ExoPlayer player, boolean isTopLevelSource, final Listener listener) {
- super.prepareSource(player, isTopLevelSource, listener);
- this.listener = listener;
+ public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
+ super.prepareSourceInternal(player, isTopLevelSource);
prepareChildSource(/* id= */ null, childSource);
}
@@ -81,9 +79,8 @@ public final class LoopingMediaSource extends CompositeMediaSource {
}
@Override
- public void releaseSource() {
- super.releaseSource();
- listener = null;
+ public void releaseSourceInternal() {
+ super.releaseSourceInternal();
childPeriodCount = 0;
}
@@ -95,7 +92,7 @@ public final class LoopingMediaSource extends CompositeMediaSource {
loopCount != Integer.MAX_VALUE
? new LoopingTimeline(timeline, loopCount)
: new InfinitelyLoopingTimeline(timeline);
- listener.onSourceInfoRefreshed(this, loopingTimeline, manifest);
+ refreshSourceInfo(loopingTimeline, manifest);
}
private static final class LoopingTimeline extends AbstractConcatenatedTimeline {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
index 02bd0cdbc7..aec8ed47af 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
@@ -25,18 +25,20 @@ import java.io.IOException;
/**
* Defines and provides media to be played by an {@link ExoPlayer}. A MediaSource has two main
* responsibilities:
+ *
*
* - To provide the player with a {@link Timeline} defining the structure of its media, and to
- * provide a new timeline whenever the structure of the media changes. The MediaSource provides
- * these timelines by calling {@link Listener#onSourceInfoRefreshed} on the {@link Listener}
- * passed to {@link #prepareSource(ExoPlayer, boolean, Listener)}.
+ * provide a new timeline whenever the structure of the media changes. The MediaSource
+ * provides these timelines by calling {@link Listener#onSourceInfoRefreshed} on the {@link
+ * Listener}s passed to {@link #prepareSource(ExoPlayer, boolean, Listener)}.
* - To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are
- * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator)}, and provide a way for the
- * player to load and read the media.
+ * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator)}, and provide a way for
+ * the player to load and read the media.
*
- * All methods are called on the player's internal playback thread, as described in the
- * {@link ExoPlayer} Javadoc. They should not be called directly from application code. Instances
- * should not be re-used, meaning they should be passed to {@link ExoPlayer#prepare} at most once.
+ *
+ * All methods are called on the player's internal playback thread, as described in the {@link
+ * ExoPlayer} Javadoc. They should not be called directly from application code. Instances can be
+ * re-used, but only for one {@link ExoPlayer} instance simultaneously.
*/
public interface MediaSource {
@@ -170,19 +172,23 @@ public interface MediaSource {
}
- String MEDIA_SOURCE_REUSED_ERROR_MESSAGE = "MediaSource instances are not allowed to be reused.";
-
/**
- * Starts preparation of the source.
- *
- * Should not be called directly from application code.
+ * Starts source preparation if not yet started, and adds a listener for timeline and/or manifest
+ * updates.
+ *
+ *
Should not be called directly from application code.
+ *
+ *
The listener will be also be notified if the source already has a timeline and/or manifest.
+ *
+ *
For each call to this method, a call to {@link #releaseSource(Listener)} is needed to remove
+ * the listener and to release the source if no longer required.
*
* @param player The player for which this source is being prepared.
- * @param isTopLevelSource Whether this source has been passed directly to
- * {@link ExoPlayer#prepare(MediaSource)} or
- * {@link ExoPlayer#prepare(MediaSource, boolean, boolean)}. If {@code false}, this source is
- * being prepared by another source (e.g. {@link ConcatenatingMediaSource}) for composition.
- * @param listener The listener for source events.
+ * @param isTopLevelSource Whether this source has been passed directly to {@link
+ * ExoPlayer#prepare(MediaSource)} or {@link ExoPlayer#prepare(MediaSource, boolean,
+ * boolean)}. If {@code false}, this source is being prepared by another source (e.g. {@link
+ * ConcatenatingMediaSource}) for composition.
+ * @param listener The listener for source info updates to be added.
*/
void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener);
@@ -216,10 +222,12 @@ public interface MediaSource {
void releasePeriod(MediaPeriod mediaPeriod);
/**
- * Releases the source.
- *
- * Should not be called directly from application code.
+ * Removes a listener for timeline and/or manifest updates and releases the source if no longer
+ * required.
+ *
+ *
Should not be called directly from application code.
+ *
+ * @param listener The listener for source info updates to be removed.
*/
- void releaseSource();
-
+ void releaseSource(Listener listener);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java
index a738cb1893..f9bf86081f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java
@@ -70,7 +70,6 @@ public final class MergingMediaSource extends CompositeMediaSource {
private final ArrayList pendingTimelineSources;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
- private Listener listener;
private Timeline primaryTimeline;
private Object primaryManifest;
private int periodCount;
@@ -98,9 +97,8 @@ public final class MergingMediaSource extends CompositeMediaSource {
}
@Override
- public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
- super.prepareSource(player, isTopLevelSource, listener);
- this.listener = listener;
+ public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
+ super.prepareSourceInternal(player, isTopLevelSource);
for (int i = 0; i < mediaSources.length; i++) {
prepareChildSource(i, mediaSources[i]);
}
@@ -132,9 +130,8 @@ public final class MergingMediaSource extends CompositeMediaSource {
}
@Override
- public void releaseSource() {
- super.releaseSource();
- listener = null;
+ public void releaseSourceInternal() {
+ super.releaseSourceInternal();
primaryTimeline = null;
primaryManifest = null;
periodCount = PERIOD_COUNT_UNSET;
@@ -158,7 +155,7 @@ public final class MergingMediaSource extends CompositeMediaSource {
primaryManifest = manifest;
}
if (pendingTimelineSources.isEmpty()) {
- listener.onSourceInfoRefreshed(this, primaryTimeline, primaryManifest);
+ refreshSourceInfo(primaryTimeline, primaryManifest);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java
index b92085d15e..445f0b882e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java
@@ -31,7 +31,7 @@ import java.io.IOException;
/**
* Loads data at a given {@link Uri} as a single sample belonging to a single {@link MediaPeriod}.
*/
-public final class SingleSampleMediaSource implements MediaSource {
+public final class SingleSampleMediaSource extends BaseMediaSource {
/**
* Listener of {@link SingleSampleMediaSource} events.
@@ -250,8 +250,8 @@ public final class SingleSampleMediaSource implements MediaSource {
// MediaSource implementation.
@Override
- public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
- listener.onSourceInfoRefreshed(this, timeline, null);
+ public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
+ refreshSourceInfo(timeline, /* manifest= */ null);
}
@Override
@@ -278,7 +278,7 @@ public final class SingleSampleMediaSource implements MediaSource {
}
@Override
- public void releaseSource() {
+ public void releaseSourceInternal() {
// Do nothing.
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
index 64bab7ed96..5f73a57e4e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
@@ -119,7 +119,6 @@ public final class AdsMediaSource extends CompositeMediaSource {
private AdPlaybackState adPlaybackState;
private MediaSource[][] adGroupMediaSources;
private long[][] adDurationsUs;
- private MediaSource.Listener listener;
/**
* Constructs a new source that inserts ads linearly with the content specified by {@code
@@ -204,11 +203,10 @@ public final class AdsMediaSource extends CompositeMediaSource {
}
@Override
- public void prepareSource(final ExoPlayer player, boolean isTopLevelSource, Listener listener) {
- super.prepareSource(player, isTopLevelSource, listener);
+ public void prepareSourceInternal(final ExoPlayer player, boolean isTopLevelSource) {
+ super.prepareSourceInternal(player, isTopLevelSource);
Assertions.checkArgument(isTopLevelSource);
final ComponentListener componentListener = new ComponentListener();
- this.listener = listener;
this.componentListener = componentListener;
prepareChildSource(new MediaPeriodId(/* periodIndex= */ 0), contentMediaSource);
mainHandler.post(new Runnable() {
@@ -276,8 +274,8 @@ public final class AdsMediaSource extends CompositeMediaSource {
}
@Override
- public void releaseSource() {
- super.releaseSource();
+ public void releaseSourceInternal() {
+ super.releaseSourceInternal();
componentListener.release();
componentListener = null;
deferredMediaPeriodByAdMediaSource.clear();
@@ -286,7 +284,6 @@ public final class AdsMediaSource extends CompositeMediaSource {
adPlaybackState = null;
adGroupMediaSources = new MediaSource[0][];
adDurationsUs = new long[0][];
- listener = null;
mainHandler.post(new Runnable() {
@Override
public void run() {
@@ -350,7 +347,7 @@ public final class AdsMediaSource extends CompositeMediaSource {
adPlaybackState.adGroupCount == 0
? contentTimeline
: new SinglePeriodAdTimeline(contentTimeline, adPlaybackState);
- listener.onSourceInfoRefreshed(this, timeline, contentManifest);
+ refreshSourceInfo(timeline, contentManifest);
}
}
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java
index fd7577b5ec..1a4d7c07a4 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java
@@ -227,9 +227,9 @@ public final class ExoPlayerTest {
MediaSource secondSource =
new FakeMediaSource(timeline, new Object(), Builder.VIDEO_FORMAT) {
@Override
- public synchronized void prepareSource(
- ExoPlayer player, boolean isTopLevelSource, Listener listener) {
- super.prepareSource(player, isTopLevelSource, listener);
+ public synchronized void prepareSourceInternal(
+ ExoPlayer player, boolean isTopLevelSource) {
+ super.prepareSourceInternal(player, isTopLevelSource);
// We've queued a source info refresh on the playback thread's event queue. Allow the
// test thread to prepare the player with the third source, and block this thread (the
// playback thread) until the test thread's call to prepare() has returned.
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 465e08b5d2..257966f5c3 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
@@ -270,6 +270,63 @@ public final class ConcatenatingMediaSourceTest {
}
}
+ @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));
+
+ testRunner.releaseSource();
+ childSource.assertReleased();
+ } finally {
+ testRunner.release();
+ }
+ }
+
+ @Test
+ public void testDuplicateNestedMediaSources() throws IOException, InterruptedException {
+ FakeMediaSource childSource =
+ new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), /* manifest= */ null);
+ ConcatenatingMediaSource nestedConcatenation =
+ new ConcatenatingMediaSource(childSource, childSource);
+ ConcatenatingMediaSource mediaSource =
+ new ConcatenatingMediaSource(childSource, nestedConcatenation, nestedConcatenation);
+ MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);
+ try {
+ Timeline timeline = testRunner.prepareSource();
+ TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1);
+
+ 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();
+ } finally {
+ testRunner.release();
+ }
+ }
+
/**
* Wraps the specified timelines in a {@link ConcatenatingMediaSource} and returns the
* concatenated timeline.
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
index 4e4628acdf..c2da872789 100644
--- 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
@@ -205,7 +205,7 @@ public final class DynamicConcatenatingMediaSourceTest {
timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET);
testRunner.assertPrepareAndReleaseAllPeriods();
- mediaSource.releaseSource();
+ testRunner.releaseSource();
for (int i = 1; i < 4; i++) {
childSources[i].assertReleased();
}
@@ -406,24 +406,6 @@ public final class DynamicConcatenatingMediaSourceTest {
} catch (NullPointerException e) {
// Expected.
}
-
- // Duplicate sources.
- mediaSource.addMediaSource(validSource);
- try {
- mediaSource.addMediaSource(validSource);
- fail("Duplicate mediaSource not allowed.");
- } catch (IllegalArgumentException e) {
- // Expected.
- }
-
- mediaSources =
- new MediaSource[] {new FakeMediaSource(createFakeTimeline(2), null), validSource};
- try {
- mediaSource.addMediaSources(Arrays.asList(mediaSources));
- fail("Duplicate mediaSource not allowed.");
- } catch (IllegalArgumentException e) {
- // Expected.
- }
}
@Test
@@ -782,6 +764,63 @@ public final class DynamicConcatenatingMediaSourceTest {
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++) {
diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
index 98783ac93e..d73ff5447f 100644
--- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
+++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java
@@ -27,6 +27,7 @@ import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.MediaPeriod;
@@ -59,7 +60,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** A DASH {@link MediaSource}. */
-public final class DashMediaSource implements MediaSource {
+public final class DashMediaSource extends BaseMediaSource {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.dash");
@@ -283,7 +284,6 @@ public final class DashMediaSource implements MediaSource {
private final PlayerEmsgCallback playerEmsgCallback;
private final LoaderErrorThrower manifestLoadErrorThrower;
- private Listener sourceListener;
private DataSource dataSource;
private Loader loader;
@@ -497,8 +497,7 @@ public final class DashMediaSource implements MediaSource {
// MediaSource implementation.
@Override
- public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
- sourceListener = listener;
+ public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
if (sideloadedManifest) {
processManifest(false);
} else {
@@ -544,7 +543,7 @@ public final class DashMediaSource implements MediaSource {
}
@Override
- public void releaseSource() {
+ public void releaseSourceInternal() {
manifestLoadPending = false;
dataSource = null;
if (loader != null) {
@@ -810,7 +809,7 @@ public final class DashMediaSource implements MediaSource {
DashTimeline timeline = new DashTimeline(manifest.availabilityStartTimeMs, windowStartTimeMs,
firstPeriodId, currentStartTimeUs, windowDurationUs, windowDefaultStartPositionUs,
manifest);
- sourceListener.onSourceInfoRefreshed(this, timeline, manifest);
+ refreshSourceInfo(timeline, manifest);
if (!sideloadedManifest) {
// Remove any pending simulated refresh.
diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
index 5113bef6e0..3259598e18 100644
--- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
+++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.MediaPeriod;
@@ -42,11 +43,9 @@ import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
import java.util.List;
-/**
- * An HLS {@link MediaSource}.
- */
-public final class HlsMediaSource implements MediaSource,
- HlsPlaylistTracker.PrimaryPlaylistListener {
+/** An HLS {@link MediaSource}. */
+public final class HlsMediaSource extends BaseMediaSource
+ implements HlsPlaylistTracker.PrimaryPlaylistListener {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.hls");
@@ -224,7 +223,6 @@ public final class HlsMediaSource implements MediaSource,
private final boolean allowChunklessPreparation;
private HlsPlaylistTracker playlistTracker;
- private Listener sourceListener;
/**
* @param manifestUri The {@link Uri} of the HLS manifest.
@@ -323,8 +321,7 @@ public final class HlsMediaSource implements MediaSource,
}
@Override
- public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
- sourceListener = listener;
+ public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher,
minLoadableRetryCount, this, playlistParser);
playlistTracker.start();
@@ -355,12 +352,11 @@ public final class HlsMediaSource implements MediaSource,
}
@Override
- public void releaseSource() {
+ public void releaseSourceInternal() {
if (playlistTracker != null) {
playlistTracker.release();
playlistTracker = null;
}
- sourceListener = null;
}
@Override
@@ -389,8 +385,7 @@ public final class HlsMediaSource implements MediaSource,
playlist.startTimeUs + playlist.durationUs, playlist.durationUs, playlist.startTimeUs,
windowDefaultStartPositionUs, true, false);
}
- sourceListener.onSourceInfoRefreshed(this, timeline,
- new HlsManifest(playlistTracker.getMasterPlaylist(), playlist));
+ refreshSourceInfo(timeline, new HlsManifest(playlistTracker.getMasterPlaylist(), playlist));
}
}
diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java
index da9024a5b5..f6973cc97e 100644
--- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java
+++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java
@@ -24,6 +24,7 @@ import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.MediaPeriod;
@@ -46,11 +47,9 @@ import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
import java.util.ArrayList;
-/**
- * A SmoothStreaming {@link MediaSource}.
- */
-public final class SsMediaSource implements MediaSource,
- Loader.Callback> {
+/** A SmoothStreaming {@link MediaSource}. */
+public final class SsMediaSource extends BaseMediaSource
+ implements Loader.Callback> {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.smoothstreaming");
@@ -256,7 +255,6 @@ public final class SsMediaSource implements MediaSource,
private final ParsingLoadable.Parser extends SsManifest> manifestParser;
private final ArrayList mediaPeriods;
- private Listener sourceListener;
private DataSource manifestDataSource;
private Loader manifestLoader;
private LoaderErrorThrower manifestLoaderErrorThrower;
@@ -418,8 +416,7 @@ public final class SsMediaSource implements MediaSource,
// MediaSource implementation.
@Override
- public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
- sourceListener = listener;
+ public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
if (sideloadedManifest) {
manifestLoaderErrorThrower = new LoaderErrorThrower.Dummy();
processManifest();
@@ -454,8 +451,7 @@ public final class SsMediaSource implements MediaSource,
}
@Override
- public void releaseSource() {
- sourceListener = null;
+ public void releaseSourceInternal() {
manifest = sideloadedManifest ? manifest : null;
manifestDataSource = null;
manifestLoadStartTimestamp = 0;
@@ -544,7 +540,7 @@ public final class SsMediaSource implements MediaSource,
timeline = new SinglePeriodTimeline(startTimeUs + durationUs, durationUs, startTimeUs, 0,
true /* isSeekable */, false /* isDynamic */);
}
- sourceListener.onSourceInfoRefreshed(this, timeline, manifest);
+ refreshSourceInfo(timeline, manifest);
}
private void scheduleManifestRefresh() {
diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java
index da81bbb62c..85e19409de 100644
--- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java
+++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java
@@ -22,6 +22,7 @@ import android.support.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroup;
@@ -33,10 +34,10 @@ import java.util.ArrayList;
import java.util.List;
/**
- * Fake {@link MediaSource} that provides a given timeline. Creating the period will return a
- * {@link FakeMediaPeriod} with a {@link TrackGroupArray} using the given {@link Format}s.
+ * Fake {@link MediaSource} that provides a given timeline. Creating the period will return a {@link
+ * FakeMediaPeriod} with a {@link TrackGroupArray} using the given {@link Format}s.
*/
-public class FakeMediaSource implements MediaSource {
+public class FakeMediaSource extends BaseMediaSource {
private final TrackGroupArray trackGroupArray;
private final ArrayList activeMediaPeriods;
@@ -46,7 +47,6 @@ public class FakeMediaSource implements MediaSource {
private Object manifest;
private boolean preparedSource;
private boolean releasedSource;
- private Listener listener;
private Handler sourceInfoRefreshHandler;
/**
@@ -75,15 +75,13 @@ public class FakeMediaSource implements MediaSource {
}
@Override
- public synchronized void prepareSource(
- ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ public synchronized void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
assertThat(preparedSource).isFalse();
preparedSource = true;
releasedSource = false;
- this.listener = listener;
sourceInfoRefreshHandler = new Handler();
if (timeline != null) {
- listener.onSourceInfoRefreshed(this, timeline, manifest);
+ refreshSourceInfo(timeline, manifest);
}
}
@@ -113,7 +111,7 @@ public class FakeMediaSource implements MediaSource {
}
@Override
- public void releaseSource() {
+ public void releaseSourceInternal() {
assertThat(preparedSource).isTrue();
assertThat(releasedSource).isFalse();
assertThat(activeMediaPeriods.isEmpty()).isTrue();
@@ -121,7 +119,6 @@ public class FakeMediaSource implements MediaSource {
preparedSource = false;
sourceInfoRefreshHandler.removeCallbacksAndMessages(null);
sourceInfoRefreshHandler = null;
- listener = null;
}
/**
@@ -138,7 +135,7 @@ public class FakeMediaSource implements MediaSource {
assertThat(preparedSource).isTrue();
timeline = newTimeline;
manifest = newManifest;
- listener.onSourceInfoRefreshed(FakeMediaSource.this, timeline, manifest);
+ refreshSourceInfo(timeline, manifest);
}
});
} else {
diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java
index 7b27d3bd80..521c8ee52a 100644
--- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java
+++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java
@@ -42,14 +42,6 @@ public final class FakeTimeline extends Timeline {
public final long durationUs;
public final AdPlaybackState adPlaybackState;
- /**
- * Creates a seekable, non-dynamic window definition with one period with a duration of
- * {@link #DEFAULT_WINDOW_DURATION_US}.
- */
- public TimelineWindowDefinition() {
- this(1, 0, true, false, DEFAULT_WINDOW_DURATION_US);
- }
-
/**
* Creates a seekable, non-dynamic window definition with a duration of
* {@link #DEFAULT_WINDOW_DURATION_US}.
@@ -217,7 +209,9 @@ public final class FakeTimeline extends Timeline {
private static TimelineWindowDefinition[] createDefaultWindowDefinitions(int windowCount) {
TimelineWindowDefinition[] windowDefinitions = new TimelineWindowDefinition[windowCount];
- Arrays.fill(windowDefinitions, new TimelineWindowDefinition());
+ for (int i = 0; i < windowCount; i++) {
+ windowDefinitions[i] = new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ i);
+ }
return windowDefinitions;
}
diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java
index fbb48c9529..ac6901463d 100644
--- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java
+++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java
@@ -193,13 +193,13 @@ public class MediaSourceTestRunner {
});
}
- /** Calls {@link MediaSource#releaseSource()} on the playback thread. */
+ /** Calls {@link MediaSource#releaseSource(Listener)} on the playback thread. */
public void releaseSource() {
runOnPlaybackThread(
new Runnable() {
@Override
public void run() {
- mediaSource.releaseSource();
+ mediaSource.releaseSource(mediaSourceListener);
}
});
}
diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java
index abef8e06be..17045f749a 100644
--- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java
+++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java
@@ -118,6 +118,7 @@ public final class TimelineAsserts {
*/
public static void assertPeriodCounts(Timeline timeline, int... expectedPeriodCounts) {
int windowCount = timeline.getWindowCount();
+ assertThat(windowCount).isEqualTo(expectedPeriodCounts.length);
int[] accumulatedPeriodCounts = new int[windowCount + 1];
accumulatedPeriodCounts[0] = 0;
for (int i = 0; i < windowCount; i++) {