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

- * 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 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++) {