mirror of
https://github.com/samsonjs/media.git
synced 2026-03-29 10:05:48 +00:00
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
This commit is contained in:
parent
b489f58cdb
commit
b2c445776a
24 changed files with 384 additions and 236 deletions
|
|
@ -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 ###
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
* <p>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<Listener> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -84,7 +84,6 @@ public final class ClippingMediaSource extends CompositeMediaSource<Void> {
|
|||
private final boolean enableInitialDiscontinuity;
|
||||
private final ArrayList<ClippingMediaPeriod> mediaPeriods;
|
||||
|
||||
private MediaSource.Listener sourceListener;
|
||||
private IllegalClippingException clippingError;
|
||||
|
||||
/**
|
||||
|
|
@ -131,9 +130,8 @@ public final class ClippingMediaSource extends CompositeMediaSource<Void> {
|
|||
}
|
||||
|
||||
@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<Void> {
|
|||
}
|
||||
|
||||
@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<Void> {
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ import java.util.HashMap;
|
|||
*
|
||||
* @param <T> The type of the id used to identify prepared child sources.
|
||||
*/
|
||||
public abstract class CompositeMediaSource<T> implements MediaSource {
|
||||
public abstract class CompositeMediaSource<T> extends BaseMediaSource {
|
||||
|
||||
private final HashMap<T, MediaSource> childSources;
|
||||
private final HashMap<T, MediaSourceAndListener> childSources;
|
||||
private ExoPlayer player;
|
||||
|
||||
/** Create composite media source without child sources. */
|
||||
|
|
@ -40,23 +40,23 @@ public abstract class CompositeMediaSource<T> 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<T> implements MediaSource {
|
|||
* this method.
|
||||
*
|
||||
* <p>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<T> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<Integer
|
|||
private final boolean isAtomic;
|
||||
private final ShuffleOrder shuffleOrder;
|
||||
|
||||
private Listener listener;
|
||||
private ConcatenatedTimeline timeline;
|
||||
|
||||
/**
|
||||
|
|
@ -85,12 +84,11 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<Integer
|
|||
}
|
||||
|
||||
@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);
|
||||
boolean[] duplicateFlags = buildDuplicateFlags(mediaSources);
|
||||
if (mediaSources.length == 0) {
|
||||
listener.onSourceInfoRefreshed(this, Timeline.EMPTY, null);
|
||||
refreshSourceInfo(Timeline.EMPTY, /* manifest= */ null);
|
||||
} else {
|
||||
for (int i = 0; i < mediaSources.length; i++) {
|
||||
if (!duplicateFlags[i]) {
|
||||
|
|
@ -118,23 +116,22 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<Integer
|
|||
}
|
||||
|
||||
@Override
|
||||
public void releaseSource() {
|
||||
super.releaseSource();
|
||||
listener = null;
|
||||
public void releaseSourceInternal() {
|
||||
super.releaseSourceInternal();
|
||||
timeline = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onChildSourceInfoRefreshed(
|
||||
Integer sourceFirstIndex,
|
||||
Integer index,
|
||||
MediaSource mediaSource,
|
||||
Timeline sourceTimeline,
|
||||
@Nullable Object sourceManifest) {
|
||||
// Set the timeline and manifest.
|
||||
timelines[sourceFirstIndex] = sourceTimeline;
|
||||
manifests[sourceFirstIndex] = sourceManifest;
|
||||
timelines[index] = sourceTimeline;
|
||||
manifests[index] = sourceManifest;
|
||||
// Also set the timeline and manifest for any duplicate entries of the same source.
|
||||
for (int i = sourceFirstIndex + 1; i < mediaSources.length; i++) {
|
||||
for (int i = index + 1; i < mediaSources.length; i++) {
|
||||
if (mediaSources[i] == mediaSource) {
|
||||
timelines[i] = sourceTimeline;
|
||||
manifests[i] = sourceManifest;
|
||||
|
|
@ -147,7 +144,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<Integer
|
|||
}
|
||||
}
|
||||
timeline = new ConcatenatedTimeline(timelines.clone(), isAtomic, shuffleOrder);
|
||||
listener.onSourceInfoRefreshed(this, timeline, manifests.clone());
|
||||
refreshSourceInfo(timeline, manifests.clone());
|
||||
}
|
||||
|
||||
private static boolean[] buildDuplicateFlags(MediaSource[] mediaSources) {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,8 @@ import java.util.Map;
|
|||
|
||||
/**
|
||||
* Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified
|
||||
* during playback. Access to this class is thread-safe.
|
||||
* during playback. It is valid for the same {@link MediaSource} instance to be present more than
|
||||
* once in the concatenation. Access to this class is thread-safe.
|
||||
*/
|
||||
public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHolder>
|
||||
implements PlayerMessage.Target {
|
||||
|
|
@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int,
|
||||
* int)} instead.
|
||||
*
|
||||
* @param index The index at which the media source will be removed. This index must be in the
|
||||
* range of 0 <= index < {@link #getSize()}.
|
||||
|
|
@ -266,10 +239,9 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource<
|
|||
|
||||
/**
|
||||
* Removes a {@link MediaSource} from the playlist and executes a custom action on completion.
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int,
|
||||
* int)} instead.
|
||||
*
|
||||
* @param index The index at which the media source will be removed. This index must be in the
|
||||
* range of 0 <= index < {@link #getSize()}.
|
||||
|
|
@ -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.<EventDispatcher>emptyList()
|
||||
: new ArrayList<>(pendingOnCompletionActions);
|
||||
pendingOnCompletionActions.clear();
|
||||
listener.onSourceInfoRefreshed(
|
||||
this,
|
||||
refreshSourceInfo(
|
||||
new ConcatenatedTimeline(
|
||||
mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic),
|
||||
/* manifest= */ null);
|
||||
|
|
|
|||
|
|
@ -35,16 +35,17 @@ import java.io.IOException;
|
|||
|
||||
/**
|
||||
* Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ public final class LoopingMediaSource extends CompositeMediaSource<Void> {
|
|||
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<Void> {
|
|||
}
|
||||
|
||||
@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<Void> {
|
|||
}
|
||||
|
||||
@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<Void> {
|
|||
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 {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>To provide the player with a {@link Timeline} defining the structure of its media, and to
|
||||
* provide a new timeline whenever the structure of the media changes. The MediaSource provides
|
||||
* these timelines by calling {@link Listener#onSourceInfoRefreshed} on the {@link Listener}
|
||||
* passed to {@link #prepareSource(ExoPlayer, boolean, Listener)}.</li>
|
||||
* provide a new timeline whenever the structure of the media changes. The MediaSource
|
||||
* provides these timelines by calling {@link Listener#onSourceInfoRefreshed} on the {@link
|
||||
* Listener}s passed to {@link #prepareSource(ExoPlayer, boolean, Listener)}.
|
||||
* <li>To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are
|
||||
* obtained by calling {@link #createPeriod(MediaPeriodId, Allocator)}, and provide a way for the
|
||||
* player to load and read the media.</li>
|
||||
* obtained by calling {@link #createPeriod(MediaPeriodId, Allocator)}, and provide a way for
|
||||
* the player to load and read the media.
|
||||
* </ul>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>Should not be called directly from application code.
|
||||
*
|
||||
* <p>The listener will be also be notified if the source already has a timeline and/or manifest.
|
||||
*
|
||||
* <p>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.
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
|
|||
private final ArrayList<MediaSource> 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<Integer> {
|
|||
}
|
||||
|
||||
@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<Integer> {
|
|||
}
|
||||
|
||||
@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<Integer> {
|
|||
primaryManifest = manifest;
|
||||
}
|
||||
if (pendingTimelineSources.isEmpty()) {
|
||||
listener.onSourceInfoRefreshed(this, primaryTimeline, primaryManifest);
|
||||
refreshSourceInfo(primaryTimeline, primaryManifest);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -119,7 +119,6 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||
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<MediaPeriodId> {
|
|||
}
|
||||
|
||||
@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<MediaPeriodId> {
|
|||
}
|
||||
|
||||
@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<MediaPeriodId> {
|
|||
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<MediaPeriodId> {
|
|||
adPlaybackState.adGroupCount == 0
|
||||
? contentTimeline
|
||||
: new SinglePeriodAdTimeline(contentTimeline, adPlaybackState);
|
||||
listener.onSourceInfoRefreshed(this, timeline, contentManifest);
|
||||
refreshSourceInfo(timeline, contentManifest);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.<MediaSource>asList(childSource, childSource));
|
||||
Timeline timeline = testRunner.assertTimelineChangeBlocking();
|
||||
|
||||
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1);
|
||||
testRunner.assertPrepareAndReleaseAllPeriods();
|
||||
assertThat(childSource.getCreatedMediaPeriods())
|
||||
.containsAllOf(
|
||||
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0),
|
||||
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2),
|
||||
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4),
|
||||
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 6),
|
||||
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1),
|
||||
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 3),
|
||||
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 5),
|
||||
new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 7));
|
||||
|
||||
testRunner.releaseSource();
|
||||
childSource.assertReleased();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDuplicateNestedMediaSources() throws IOException, InterruptedException {
|
||||
FakeMediaSource childSource =
|
||||
new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), /* manifest= */ null);
|
||||
DynamicConcatenatingMediaSource nestedConcatenation = new DynamicConcatenatingMediaSource();
|
||||
|
||||
testRunner.prepareSource();
|
||||
mediaSource.addMediaSources(
|
||||
Arrays.<MediaSource>asList(childSource, nestedConcatenation, nestedConcatenation));
|
||||
testRunner.assertTimelineChangeBlocking();
|
||||
nestedConcatenation.addMediaSource(childSource);
|
||||
testRunner.assertTimelineChangeBlocking();
|
||||
nestedConcatenation.addMediaSource(childSource);
|
||||
Timeline timeline = testRunner.assertTimelineChangeBlocking();
|
||||
|
||||
TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1);
|
||||
testRunner.assertPrepareAndReleaseAllPeriods();
|
||||
assertThat(childSource.getCreatedMediaPeriods())
|
||||
.containsAllOf(
|
||||
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0),
|
||||
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1),
|
||||
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2),
|
||||
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 3),
|
||||
new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4));
|
||||
|
||||
testRunner.releaseSource();
|
||||
childSource.assertReleased();
|
||||
}
|
||||
|
||||
private static FakeMediaSource[] createMediaSources(int count) {
|
||||
FakeMediaSource[] sources = new FakeMediaSource[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ParsingLoadable<SsManifest>> {
|
||||
/** A SmoothStreaming {@link MediaSource}. */
|
||||
public final class SsMediaSource extends BaseMediaSource
|
||||
implements Loader.Callback<ParsingLoadable<SsManifest>> {
|
||||
|
||||
static {
|
||||
ExoPlayerLibraryInfo.registerModule("goog.exo.smoothstreaming");
|
||||
|
|
@ -256,7 +255,6 @@ public final class SsMediaSource implements MediaSource,
|
|||
private final ParsingLoadable.Parser<? extends SsManifest> manifestParser;
|
||||
private final ArrayList<SsMediaPeriod> 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() {
|
||||
|
|
|
|||
|
|
@ -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<FakeMediaPeriod> 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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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++) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue