Refactor #6.1: Pull up manifest requests.

This change pulls manifest refresh responsibility up to the
top level Dash/SS SampleSource implementations. In following
steps more of the manifest processing logic will be pulled
up (e.g. extracting track groups from the initial manifest),
which will allow ChunkSampleSource/ChunkSource instances to
be further simplified and created on demand.

I've avoided moving/renaming anything for now, so as to keep
it fairly easy to review.

Note that this change does the TODO related to releasing the
manifest fetchers.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=121001139
This commit is contained in:
olly 2016-04-28 02:31:04 -07:00 committed by Oliver Woodman
parent 15a890c3ce
commit b38d004553
6 changed files with 144 additions and 153 deletions

View File

@ -135,9 +135,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
if (prepared) {
return true;
}
if (!chunkSource.prepare()) {
return false;
}
TrackGroup tracks = chunkSource.getTracks();
if (tracks != null) {
trackGroups = new TrackGroupArray(tracks);
@ -207,7 +204,6 @@ public class ChunkSampleSource implements SampleSource, TrackStream, Loader.Call
@Override
public void continueBuffering(long positionUs) {
downstreamPositionUs = positionUs;
chunkSource.continueBuffering();
if (!loader.isLoading()) {
maybeStartLoading();
}

View File

@ -41,16 +41,6 @@ public interface ChunkSource {
*/
void maybeThrowError() throws IOException;
/**
* Prepares the source.
* <p>
* The method can be called repeatedly until the return value indicates success.
*
* @return True if the source was prepared, false otherwise.
* @throws IOException If an error occurs preparing the source.
*/
boolean prepare() throws IOException;
/**
* Gets the duration of the source in microseconds.
* <p>
@ -80,13 +70,6 @@ public interface ChunkSource {
*/
void enable(int[] tracks);
/**
* Indicates to the source that it should still be checking for updates to the stream.
* <p>
* This method should only be called when the source is enabled.
*/
void continueBuffering();
/**
* Evaluates whether {@link MediaChunk}s should be removed from the back of the queue.
* <p>

View File

@ -43,7 +43,6 @@ import com.google.android.exoplayer.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer.extractor.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util;
@ -71,7 +70,6 @@ public class DashChunkSource implements ChunkSource {
private final DataSource dataSource;
private final FormatEvaluator adaptiveFormatEvaluator;
private final Evaluation evaluation;
private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
// Properties of the initial manifest.
private boolean live;
@ -93,15 +91,13 @@ public class DashChunkSource implements ChunkSource {
private boolean[] adaptiveFormatBlacklistFlags;
/**
* @param manifestFetcher A fetcher for the manifest.
* @param adaptationSetType The type of the adaptation set exposed by this source. One of
* {@link C#TRACK_TYPE_AUDIO}, {@link C#TRACK_TYPE_VIDEO} and {@link C#TRACK_TYPE_TEXT}.
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats.
*/
public DashChunkSource(ManifestFetcher<MediaPresentationDescription> manifestFetcher,
int adaptationSetType, DataSource dataSource, FormatEvaluator adaptiveFormatEvaluator) {
this.manifestFetcher = manifestFetcher;
public DashChunkSource(int adaptationSetType, DataSource dataSource,
FormatEvaluator adaptiveFormatEvaluator) {
this.adaptationSetType = adaptationSetType;
this.dataSource = dataSource;
this.adaptiveFormatEvaluator = adaptiveFormatEvaluator;
@ -114,24 +110,12 @@ public class DashChunkSource implements ChunkSource {
public void maybeThrowError() throws IOException {
if (fatalError != null) {
throw fatalError;
} else if (live) {
manifestFetcher.maybeThrowError();
}
}
@Override
public boolean prepare() throws IOException {
if (currentManifest == null) {
currentManifest = manifestFetcher.getManifest();
if (currentManifest == null) {
manifestFetcher.maybeThrowError();
manifestFetcher.requestRefresh();
return false;
} else {
initForManifest(currentManifest);
}
}
return true;
public void init(MediaPresentationDescription initialManifest) {
currentManifest = initialManifest;
initForManifest(currentManifest);
}
@Override
@ -155,33 +139,10 @@ public class DashChunkSource implements ChunkSource {
adaptiveFormatEvaluator.enable(enabledFormats);
adaptiveFormatBlacklistFlags = new boolean[tracks.length];
}
processManifest(manifestFetcher.getManifest());
}
@Override
public void continueBuffering() {
if (!currentManifest.dynamic || fatalError != null) {
return;
}
MediaPresentationDescription newManifest = manifestFetcher.getManifest();
if (newManifest != null && newManifest != currentManifest) {
processManifest(newManifest);
}
// TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where
// minUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is explicit
// signaling in the stream, according to:
// http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service/
long minUpdatePeriod = currentManifest.minUpdatePeriod;
if (minUpdatePeriod == 0) {
minUpdatePeriod = 5000;
}
if (android.os.SystemClock.elapsedRealtime()
> manifestFetcher.getManifestLoadStartTimestamp() + minUpdatePeriod) {
manifestFetcher.requestRefresh();
}
public void updateManifest(MediaPresentationDescription newManifest) {
processManifest(newManifest);
}
@Override

View File

@ -25,7 +25,6 @@ import com.google.android.exoplayer.TrackSelection;
import com.google.android.exoplayer.TrackStream;
import com.google.android.exoplayer.chunk.ChunkSampleSource;
import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener;
import com.google.android.exoplayer.chunk.ChunkSource;
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser;
@ -38,6 +37,7 @@ import com.google.android.exoplayer.util.ManifestFetcher;
import android.net.Uri;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Pair;
import java.io.IOException;
@ -50,10 +50,13 @@ import java.util.List;
*/
public final class DashSampleSource implements SampleSource {
private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
private final DashChunkSource[] chunkSources;
private final SampleSource[] sources;
private final IdentityHashMap<TrackStream, SampleSource> trackStreamSources;
private final int[] selectedTrackCounts;
private MediaPresentationDescription currentManifest;
private boolean prepared;
private boolean seenFirstTrackSelection;
private long durationUs;
@ -65,34 +68,32 @@ public final class DashSampleSource implements SampleSource {
ChunkSampleSourceEventListener eventListener) {
MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser();
DataSource manifestDataSource = dataSourceFactory.createDataSource();
// TODO[REFACTOR]: This needs releasing.
ManifestFetcher<MediaPresentationDescription> manifestFetcher = new ManifestFetcher<>(uri,
manifestDataSource, parser);
manifestFetcher = new ManifestFetcher<>(uri, manifestDataSource, parser);
LoadControl loadControl = new DefaultLoadControl(
new DefaultAllocator(C.DEFAULT_BUFFER_SEGMENT_SIZE));
// Build the video renderer.
DataSource videoDataSource = dataSourceFactory.createDataSource(bandwidthMeter);
ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, C.TRACK_TYPE_VIDEO,
videoDataSource, new AdaptiveEvaluator(bandwidthMeter));
DashChunkSource videoChunkSource = new DashChunkSource(C.TRACK_TYPE_VIDEO, videoDataSource,
new AdaptiveEvaluator(bandwidthMeter));
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
C.DEFAULT_VIDEO_BUFFER_SIZE, eventHandler, eventListener, C.TRACK_TYPE_VIDEO);
// Build the audio renderer.
DataSource audioDataSource = dataSourceFactory.createDataSource(bandwidthMeter);
ChunkSource audioChunkSource = new DashChunkSource(manifestFetcher, C.TRACK_TYPE_AUDIO,
audioDataSource, null);
DashChunkSource audioChunkSource = new DashChunkSource(C.TRACK_TYPE_AUDIO, audioDataSource,
null);
ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
C.DEFAULT_AUDIO_BUFFER_SIZE, eventHandler, eventListener, C.TRACK_TYPE_AUDIO);
// Build the text renderer.
DataSource textDataSource = dataSourceFactory.createDataSource(bandwidthMeter);
ChunkSource textChunkSource = new DashChunkSource(manifestFetcher, C.TRACK_TYPE_TEXT,
textDataSource, null);
DashChunkSource textChunkSource = new DashChunkSource(C.TRACK_TYPE_TEXT, textDataSource, null);
ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
C.DEFAULT_TEXT_BUFFER_SIZE, eventHandler, eventListener, C.TRACK_TYPE_TEXT);
chunkSources = new DashChunkSource[] {videoChunkSource, audioChunkSource, textChunkSource};
sources = new SampleSource[] {videoSampleSource, audioSampleSource, textSampleSource};
trackStreamSources = new IdentityHashMap<>();
selectedTrackCounts = new int[sources.length];
@ -103,6 +104,20 @@ public final class DashSampleSource implements SampleSource {
if (prepared) {
return true;
}
if (currentManifest == null) {
currentManifest = manifestFetcher.getManifest();
if (currentManifest == null) {
manifestFetcher.maybeThrowError();
manifestFetcher.requestRefresh();
return false;
} else {
for (DashChunkSource chunkSource : chunkSources) {
chunkSource.init(currentManifest);
}
}
}
boolean sourcesPrepared = true;
for (SampleSource source : sources) {
sourcesPrepared &= source.prepare(positionUs);
@ -171,6 +186,30 @@ public final class DashSampleSource implements SampleSource {
@Override
public void continueBuffering(long positionUs) {
if (currentManifest.dynamic) {
MediaPresentationDescription newManifest = manifestFetcher.getManifest();
if (newManifest != currentManifest) {
currentManifest = newManifest;
for (DashChunkSource chunkSource : chunkSources) {
chunkSource.updateManifest(newManifest);
}
}
long minUpdatePeriod = currentManifest.minUpdatePeriod;
if (minUpdatePeriod == 0) {
// TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where
// minUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is explicit
// signaling in the stream, according to:
// http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service/
minUpdatePeriod = 5000;
}
if (SystemClock.elapsedRealtime() > manifestFetcher.getManifestLoadStartTimestamp()
+ minUpdatePeriod) {
manifestFetcher.requestRefresh();
}
}
for (SampleSource source : enabledSources) {
source.continueBuffering(positionUs);
}
@ -215,6 +254,7 @@ public final class DashSampleSource implements SampleSource {
@Override
public void release() {
manifestFetcher.release();
for (SampleSource source : sources) {
source.release();
}

View File

@ -37,11 +37,9 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.Prot
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.MimeTypes;
import android.net.Uri;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Base64;
@ -55,16 +53,13 @@ import java.util.List;
// TODO[REFACTOR]: Handle multiple stream elements of the same type (at a higher level).
public class SmoothStreamingChunkSource implements ChunkSource {
private static final int MINIMUM_MANIFEST_REFRESH_PERIOD_MS = 5000;
private static final int INITIALIZATION_VECTOR_SIZE = 8;
private final int streamElementType;
private final DataSource dataSource;
private final Evaluation evaluation;
private final ManifestFetcher<SmoothStreamingManifest> manifestFetcher;
private final FormatEvaluator adaptiveFormatEvaluator;
private boolean live;
private long durationUs;
private TrackEncryptionBox[] trackEncryptionBoxes;
private DrmInitData.Mapped drmInitData;
@ -84,59 +79,48 @@ public class SmoothStreamingChunkSource implements ChunkSource {
private IOException fatalError;
/**
* @param manifestFetcher A fetcher for the manifest.
* @param streamElementType The type of stream element exposed by this source. One of
* {@link C#TRACK_TYPE_VIDEO}, {@link C#TRACK_TYPE_AUDIO} and {@link C#TRACK_TYPE_TEXT}.
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats.
*/
public SmoothStreamingChunkSource(ManifestFetcher<SmoothStreamingManifest> manifestFetcher,
int streamElementType, DataSource dataSource, FormatEvaluator adaptiveFormatEvaluator) {
this.manifestFetcher = manifestFetcher;
public SmoothStreamingChunkSource(int streamElementType, DataSource dataSource,
FormatEvaluator adaptiveFormatEvaluator) {
this.streamElementType = streamElementType;
this.dataSource = dataSource;
this.adaptiveFormatEvaluator = adaptiveFormatEvaluator;
evaluation = new Evaluation();
}
public boolean needManifestRefresh() {
return needManifestRefresh;
}
// ChunkSource implementation.
@Override
public void maybeThrowError() throws IOException {
if (fatalError != null) {
throw fatalError;
} else if (live) {
manifestFetcher.maybeThrowError();
}
}
@Override
public boolean prepare() throws IOException {
if (currentManifest == null) {
currentManifest = manifestFetcher.getManifest();
if (currentManifest == null) {
manifestFetcher.maybeThrowError();
manifestFetcher.requestRefresh();
return false;
} else {
live = currentManifest.isLive;
durationUs = currentManifest.durationUs;
ProtectionElement protectionElement = currentManifest.protectionElement;
if (protectionElement != null) {
byte[] keyId = getProtectionElementKeyId(protectionElement.data);
trackEncryptionBoxes = new TrackEncryptionBox[1];
trackEncryptionBoxes[0] = new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId);
drmInitData = new DrmInitData.Mapped();
drmInitData.put(protectionElement.uuid,
new SchemeInitData(MimeTypes.VIDEO_MP4, protectionElement.data));
} else {
trackEncryptionBoxes = null;
drmInitData = null;
}
initForManifest(currentManifest);
}
public void init(SmoothStreamingManifest initialManifest) {
currentManifest = initialManifest;
durationUs = currentManifest.durationUs;
ProtectionElement protectionElement = currentManifest.protectionElement;
if (protectionElement != null) {
byte[] keyId = getProtectionElementKeyId(protectionElement.data);
trackEncryptionBoxes = new TrackEncryptionBox[1];
trackEncryptionBoxes[0] = new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId);
drmInitData = new DrmInitData.Mapped();
drmInitData.put(protectionElement.uuid,
new SchemeInitData(MimeTypes.VIDEO_MP4, protectionElement.data));
} else {
trackEncryptionBoxes = null;
drmInitData = null;
}
return true;
initForManifest(currentManifest);
}
@Override
@ -162,40 +146,27 @@ public class SmoothStreamingChunkSource implements ChunkSource {
}
}
@Override
public void continueBuffering() {
if (!currentManifest.isLive || fatalError != null) {
return;
}
SmoothStreamingManifest newManifest = manifestFetcher.getManifest();
if (currentManifest != newManifest && newManifest != null) {
StreamElement currentElement = currentManifest.streamElements[elementIndex];
int currentElementChunkCount = currentElement.chunkCount;
StreamElement newElement = newManifest.streamElements[elementIndex];
if (currentElementChunkCount == 0 || newElement.chunkCount == 0) {
// There's no overlap between the old and new elements because at least one is empty.
public void updateManifest(SmoothStreamingManifest newManifest) {
StreamElement currentElement = currentManifest.streamElements[elementIndex];
int currentElementChunkCount = currentElement.chunkCount;
StreamElement newElement = newManifest.streamElements[elementIndex];
if (currentElementChunkCount == 0 || newElement.chunkCount == 0) {
// There's no overlap between the old and new elements because at least one is empty.
currentManifestChunkOffset += currentElementChunkCount;
} else {
long currentElementEndTimeUs = currentElement.getStartTimeUs(currentElementChunkCount - 1)
+ currentElement.getChunkDurationUs(currentElementChunkCount - 1);
long newElementStartTimeUs = newElement.getStartTimeUs(0);
if (currentElementEndTimeUs <= newElementStartTimeUs) {
// There's no overlap between the old and new elements.
currentManifestChunkOffset += currentElementChunkCount;
} else {
long currentElementEndTimeUs = currentElement.getStartTimeUs(currentElementChunkCount - 1)
+ currentElement.getChunkDurationUs(currentElementChunkCount - 1);
long newElementStartTimeUs = newElement.getStartTimeUs(0);
if (currentElementEndTimeUs <= newElementStartTimeUs) {
// There's no overlap between the old and new elements.
currentManifestChunkOffset += currentElementChunkCount;
} else {
// The new element overlaps with the old one.
currentManifestChunkOffset += currentElement.getChunkIndex(newElementStartTimeUs);
}
// The new element overlaps with the old one.
currentManifestChunkOffset += currentElement.getChunkIndex(newElementStartTimeUs);
}
currentManifest = newManifest;
needManifestRefresh = false;
}
if (needManifestRefresh && (SystemClock.elapsedRealtime()
> manifestFetcher.getManifestLoadStartTimestamp() + MINIMUM_MANIFEST_REFRESH_PERIOD_MS)) {
manifestFetcher.requestRefresh();
}
currentManifest = newManifest;
needManifestRefresh = false;
}
@Override

View File

@ -25,7 +25,6 @@ import com.google.android.exoplayer.TrackSelection;
import com.google.android.exoplayer.TrackStream;
import com.google.android.exoplayer.chunk.ChunkSampleSource;
import com.google.android.exoplayer.chunk.ChunkSampleSourceEventListener;
import com.google.android.exoplayer.chunk.ChunkSource;
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
import com.google.android.exoplayer.upstream.BandwidthMeter;
import com.google.android.exoplayer.upstream.DataSource;
@ -37,6 +36,7 @@ import com.google.android.exoplayer.util.Util;
import android.net.Uri;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Pair;
import java.io.IOException;
@ -49,10 +49,15 @@ import java.util.List;
*/
public final class SmoothStreamingSampleSource implements SampleSource {
private static final int MINIMUM_MANIFEST_REFRESH_PERIOD_MS = 5000;
private final ManifestFetcher<SmoothStreamingManifest> manifestFetcher;
private final SmoothStreamingChunkSource[] chunkSources;
private final SampleSource[] sources;
private final IdentityHashMap<TrackStream, SampleSource> trackStreamSources;
private final int[] selectedTrackCounts;
private SmoothStreamingManifest currentManifest;
private boolean prepared;
private boolean seenFirstTrackSelection;
private long durationUs;
@ -67,33 +72,33 @@ public final class SmoothStreamingSampleSource implements SampleSource {
}
SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser();
DataSource manifestDataSource = dataSourceFactory.createDataSource();
// TODO[REFACTOR]: This needs releasing.
ManifestFetcher<SmoothStreamingManifest> manifestFetcher = new ManifestFetcher<>(uri,
manifestDataSource, parser);
manifestFetcher = new ManifestFetcher<>(uri, manifestDataSource, parser);
LoadControl loadControl = new DefaultLoadControl(
new DefaultAllocator(C.DEFAULT_BUFFER_SEGMENT_SIZE));
// Build the video renderer.
DataSource videoDataSource = dataSourceFactory.createDataSource(bandwidthMeter);
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
C.TRACK_TYPE_VIDEO, videoDataSource, new AdaptiveEvaluator(bandwidthMeter));
SmoothStreamingChunkSource videoChunkSource = new SmoothStreamingChunkSource(C.TRACK_TYPE_VIDEO,
videoDataSource, new AdaptiveEvaluator(bandwidthMeter));
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
C.DEFAULT_VIDEO_BUFFER_SIZE, eventHandler, eventListener, C.TRACK_TYPE_VIDEO);
// Build the audio renderer.
DataSource audioDataSource = dataSourceFactory.createDataSource(bandwidthMeter);
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
C.TRACK_TYPE_AUDIO, audioDataSource, null);
SmoothStreamingChunkSource audioChunkSource = new SmoothStreamingChunkSource(C.TRACK_TYPE_AUDIO,
audioDataSource, null);
ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
C.DEFAULT_AUDIO_BUFFER_SIZE, eventHandler, eventListener, C.TRACK_TYPE_AUDIO);
// Build the text renderer.
DataSource textDataSource = dataSourceFactory.createDataSource(bandwidthMeter);
ChunkSource textChunkSource = new SmoothStreamingChunkSource(manifestFetcher, C.TRACK_TYPE_TEXT,
SmoothStreamingChunkSource textChunkSource = new SmoothStreamingChunkSource(C.TRACK_TYPE_TEXT,
textDataSource, null);
ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
C.DEFAULT_TEXT_BUFFER_SIZE, eventHandler, eventListener, C.TRACK_TYPE_TEXT);
chunkSources = new SmoothStreamingChunkSource[] {videoChunkSource, audioChunkSource,
textChunkSource};
sources = new SampleSource[] {videoSampleSource, audioSampleSource, textSampleSource};
trackStreamSources = new IdentityHashMap<>();
selectedTrackCounts = new int[sources.length];
@ -104,6 +109,20 @@ public final class SmoothStreamingSampleSource implements SampleSource {
if (prepared) {
return true;
}
if (currentManifest == null) {
currentManifest = manifestFetcher.getManifest();
if (currentManifest == null) {
manifestFetcher.maybeThrowError();
manifestFetcher.requestRefresh();
return false;
} else {
for (SmoothStreamingChunkSource chunkSource : chunkSources) {
chunkSource.init(currentManifest);
}
}
}
boolean sourcesPrepared = true;
for (SampleSource source : sources) {
sourcesPrepared &= source.prepare(positionUs);
@ -172,6 +191,26 @@ public final class SmoothStreamingSampleSource implements SampleSource {
@Override
public void continueBuffering(long positionUs) {
if (currentManifest.isLive) {
SmoothStreamingManifest newManifest = manifestFetcher.getManifest();
if (newManifest != currentManifest) {
currentManifest = newManifest;
for (SmoothStreamingChunkSource chunkSource : chunkSources) {
chunkSource.updateManifest(newManifest);
}
}
if (SystemClock.elapsedRealtime()
> manifestFetcher.getManifestLoadStartTimestamp() + MINIMUM_MANIFEST_REFRESH_PERIOD_MS) {
for (SmoothStreamingChunkSource chunkSource : chunkSources) {
if (chunkSource.needManifestRefresh()) {
manifestFetcher.requestRefresh();
break;
}
}
}
}
for (SampleSource source : enabledSources) {
source.continueBuffering(positionUs);
}
@ -216,6 +255,7 @@ public final class SmoothStreamingSampleSource implements SampleSource {
@Override
public void release() {
manifestFetcher.release();
for (SampleSource source : sources) {
source.release();
}