From b2c445776aaab420e7b971d2f2feb95846c41624 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 27 Feb 2018 09:29:49 -0800 Subject: [PATCH] Allow parallel reuse of media sources. This is achieved by adding a BaseMediaSource which keeps a reference count of the number of times the source has been prepared and forwards to the actual implementations only once, such that only minimal changes are needed for each media source. Issue:#3498 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=187186691 --- RELEASENOTES.md | 3 + .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 21 ++-- .../exoplayer2/ExoPlayerImplInternal.java | 2 +- .../exoplayer2/source/BaseMediaSource.java | 95 +++++++++++++++++++ .../source/ClippingMediaSource.java | 13 +-- .../source/CompositeMediaSource.java | 48 ++++++---- .../source/ConcatenatingMediaSource.java | 23 ++--- .../DynamicConcatenatingMediaSource.java | 58 +++-------- .../source/ExtractorMediaSource.java | 32 +++---- .../exoplayer2/source/LoopingMediaSource.java | 13 +-- .../exoplayer2/source/MediaSource.java | 54 ++++++----- .../exoplayer2/source/MergingMediaSource.java | 13 +-- .../source/SingleSampleMediaSource.java | 8 +- .../exoplayer2/source/ads/AdsMediaSource.java | 13 +-- .../android/exoplayer2/ExoPlayerTest.java | 6 +- .../source/ConcatenatingMediaSourceTest.java | 57 +++++++++++ .../DynamicConcatenatingMediaSourceTest.java | 77 +++++++++++---- .../source/dash/DashMediaSource.java | 11 +-- .../exoplayer2/source/hls/HlsMediaSource.java | 19 ++-- .../source/smoothstreaming/SsMediaSource.java | 18 ++-- .../exoplayer2/testutil/FakeMediaSource.java | 19 ++-- .../exoplayer2/testutil/FakeTimeline.java | 12 +-- .../testutil/MediaSourceTestRunner.java | 4 +- .../exoplayer2/testutil/TimelineAsserts.java | 1 + 24 files changed, 384 insertions(+), 236 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java 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++) {