diff --git a/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java b/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java index a01cc60783..c8ef102935 100644 --- a/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java @@ -68,6 +68,7 @@ public final class SingleSampleSource implements SampleSource, TrackStream, private final Loader loader; private final Format format; private final long durationUs; + private final int minLoadableRetryCount; private final TrackGroupArray tracks; private final Handler eventHandler; private final EventListener eventListener; @@ -96,10 +97,11 @@ public final class SingleSampleSource implements SampleSource, TrackStream, this.dataSource = dataSource; this.format = format; this.durationUs = durationUs; + this.minLoadableRetryCount = minLoadableRetryCount; this.eventHandler = eventHandler; this.eventListener = eventListener; this.eventSourceId = eventSourceId; - loader = new Loader("Loader:SingleSampleSource", minLoadableRetryCount); + loader = new Loader("Loader:SingleSampleSource"); tracks = new TrackGroupArray(new TrackGroup(format)); sampleData = new byte[INITIAL_SAMPLE_SIZE]; } @@ -268,7 +270,7 @@ public final class SingleSampleSource implements SampleSource, TrackStream, if (loadingFinished || streamState == STREAM_STATE_END_OF_STREAM || loader.isLoading()) { return; } - loader.startLoading(this, this); + loader.startLoading(this, this, minLoadableRetryCount); } private void notifyLoadError(final IOException e) { diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkTrackStream.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkTrackStream.java index a61b47c60b..1b6366aa6c 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkTrackStream.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkTrackStream.java @@ -41,6 +41,7 @@ public class ChunkTrackStream implements TrackStream, Loader.Callback { private final Loader loader; private final ChunkSource chunkSource; + private final int minLoadableRetryCount; private final LinkedList mediaChunks; private final List readOnlyMediaChunks; private final DefaultTrackOutput sampleQueue; @@ -76,7 +77,8 @@ public class ChunkTrackStream implements TrackStream, Loader.Callback { ChunkTrackStreamEventListener eventListener, int eventSourceId, int minLoadableRetryCount) { this.chunkSource = chunkSource; this.loadControl = loadControl; - loader = new Loader("Loader:ChunkTrackStream", minLoadableRetryCount); + this.minLoadableRetryCount = minLoadableRetryCount; + loader = new Loader("Loader:ChunkTrackStream"); eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId); nextChunkHolder = new ChunkHolder(); mediaChunks = new LinkedList<>(); @@ -317,7 +319,7 @@ public class ChunkTrackStream implements TrackStream, Loader.Callback { eventDispatcher.loadStarted(loadable.dataSpec.length, loadable.type, loadable.trigger, loadable.format, -1, -1); } - loader.startLoading(loadable, this); + loader.startLoading(loadable, this, minLoadableRetryCount); // Update the load control again to indicate that we're now loading. loadControl.update(this, downstreamPositionUs, getNextLoadPositionUs(), true); } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashSampleSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashSampleSource.java index a7c3c3ff87..bc7d7e550e 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashSampleSource.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer.C; import com.google.android.exoplayer.DefaultLoadControl; import com.google.android.exoplayer.Format; import com.google.android.exoplayer.LoadControl; +import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackGroup; import com.google.android.exoplayer.TrackGroupArray; @@ -34,13 +35,12 @@ import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser; import com.google.android.exoplayer.dash.mpd.Period; import com.google.android.exoplayer.dash.mpd.Representation; import com.google.android.exoplayer.dash.mpd.UtcTimingElement; -import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver; -import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver.UtcTimingCallback; import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSourceFactory; import com.google.android.exoplayer.upstream.DefaultAllocator; -import com.google.android.exoplayer.util.ManifestFetcher; +import com.google.android.exoplayer.upstream.Loader; +import com.google.android.exoplayer.upstream.UriLoadable; import com.google.android.exoplayer.util.Util; import android.net.Uri; @@ -49,35 +49,46 @@ import android.os.SystemClock; import android.util.Log; import android.util.Pair; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.List; +import java.util.Locale; +import java.util.TimeZone; /** * A {@link SampleSource} for DASH media. */ -public final class DashSampleSource implements SampleSource, UtcTimingCallback { +public final class DashSampleSource implements SampleSource { private static final String TAG = "DashSampleSource"; /** * The minimum number of times to retry loading data prior to failing. */ - // TODO: Use this for manifest loads as well. private static final int MIN_LOADABLE_RETRY_COUNT = 3; - private final ManifestFetcher manifestFetcher; private final DataSourceFactory dataSourceFactory; private final BandwidthMeter bandwidthMeter; private final Handler eventHandler; private final ChunkTrackStreamEventListener eventListener; private final LoadControl loadControl; + private final Loader loader; + private final DataSource dataSource; + private final MediaPresentationDescriptionParser manifestParser; + private final ManifestCallback manifestCallback; + + private Uri manifestUri; + private long manifestLoadTimestamp; + private MediaPresentationDescription manifest; private boolean prepared; - private boolean released; private long durationUs; private long elapsedRealtimeOffset; - private MediaPresentationDescription manifest; private TrackGroupArray trackGroups; private int[] trackGroupAdaptationSetIndices; private boolean pendingReset; @@ -86,21 +97,21 @@ public final class DashSampleSource implements SampleSource, UtcTimingCallback { private DashChunkSource[] chunkSources; private ChunkTrackStream[] trackStreams; - public DashSampleSource(Uri uri, DataSourceFactory dataSourceFactory, + public DashSampleSource(Uri manifestUri, DataSourceFactory dataSourceFactory, BandwidthMeter bandwidthMeter, Handler eventHandler, ChunkTrackStreamEventListener eventListener) { + this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.bandwidthMeter = bandwidthMeter; this.eventHandler = eventHandler; this.eventListener = eventListener; - loadControl = new DefaultLoadControl(new DefaultAllocator(C.DEFAULT_BUFFER_SEGMENT_SIZE)); + loader = new Loader("Loader:DashSampleSource"); + dataSource = dataSourceFactory.createDataSource(); + manifestParser = new MediaPresentationDescriptionParser(); + manifestCallback = new ManifestCallback(); chunkSources = new DashChunkSource[0]; trackStreams = new ChunkTrackStream[0]; - - MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); - DataSource manifestDataSource = dataSourceFactory.createDataSource(); - manifestFetcher = new ManifestFetcher<>(uri, manifestDataSource, parser); } @Override @@ -108,25 +119,11 @@ public final class DashSampleSource implements SampleSource, UtcTimingCallback { if (prepared) { return true; } - - if (manifest == null) { - manifest = manifestFetcher.getManifest(); - if (manifest == null) { - manifestFetcher.maybeThrowError(); - manifestFetcher.requestRefresh(); - return false; - } - durationUs = manifest.dynamic ? C.UNSET_TIME_US : manifest.duration * 1000; - buildTrackGroups(manifest); - if (manifest.utcTiming != null) { - UtcTimingElementResolver.resolveTimingElement(dataSourceFactory.createDataSource(), - manifest.utcTiming, manifestFetcher.getManifestLoadCompleteTimestamp(), this); - } else { - prepared = true; - } + loader.maybeThrowError(); + if (!loader.isLoading() && manifest == null) { + startLoadingManifest(); } - - return prepared; + return false; } @Override @@ -177,14 +174,6 @@ public final class DashSampleSource implements SampleSource, UtcTimingCallback { @Override public void continueBuffering(long positionUs) { if (manifest.dynamic) { - MediaPresentationDescription newManifest = manifestFetcher.getManifest(); - if (newManifest != manifest) { - manifest = newManifest; - for (DashChunkSource chunkSource : chunkSources) { - chunkSource.updateManifest(newManifest); - } - } - long minUpdatePeriod = manifest.minUpdatePeriod; if (minUpdatePeriod == 0) { // TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where @@ -193,13 +182,11 @@ public final class DashSampleSource implements SampleSource, UtcTimingCallback { // 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(); + if (!loader.isLoading() + && SystemClock.elapsedRealtime() > manifestLoadTimestamp + minUpdatePeriod) { + startLoadingManifest(); } } - for (ChunkTrackStream trackStream : trackStreams) { trackStream.continueBuffering(positionUs); } @@ -241,39 +228,87 @@ public final class DashSampleSource implements SampleSource, UtcTimingCallback { @Override public void release() { - manifestFetcher.release(); + loader.release(); for (DashChunkSource chunkSource : chunkSources) { chunkSource.release(); } for (ChunkTrackStream trackStream : trackStreams) { trackStream.release(); } - released = true; } - // UtcTimingCallback implementation. + // Loadable callbacks. - @Override - public void onTimestampResolved(UtcTimingElement utcTiming, long elapsedRealtimeOffset) { - if (released) { - return; + /* package */ void onManifestLoadCompleted(MediaPresentationDescription manifest) { + this.manifest = manifest; + manifestLoadTimestamp = SystemClock.elapsedRealtime(); + if (manifest.location != null) { + manifestUri = manifest.location; } + if (!prepared) { + durationUs = manifest.dynamic ? C.UNSET_TIME_US : manifest.duration * 1000; + buildTrackGroups(manifest); + if (manifest.utcTiming != null) { + resolveUtcTimingElement(manifest.utcTiming); + } else { + prepared = true; + } + } else { + for (DashChunkSource chunkSource : chunkSources) { + chunkSource.updateManifest(manifest); + } + } + } + + /* package */ void onUtcTimestampLoadCompleted(long elapsedRealtimeOffset) { this.elapsedRealtimeOffset = elapsedRealtimeOffset; prepared = true; } - @Override - public void onTimestampError(UtcTimingElement utcTiming, IOException e) { - if (released) { - return; - } - Log.e(TAG, "Failed to resolve UtcTiming element [" + utcTiming + "]", e); + /* package */ void onUtcTimestampLoadError(IOException e) { + Log.e(TAG, "Failed to resolve UtcTiming element.", e); // Be optimistic and continue in the hope that the device clock is correct. prepared = true; } // Internal methods. + private void startLoadingManifest() { + loader.startLoading(new UriLoadable<>(manifestUri, dataSource, manifestParser), + manifestCallback, MIN_LOADABLE_RETRY_COUNT); + } + + private void resolveUtcTimingElement(UtcTimingElement timingElement) { + String scheme = timingElement.schemeIdUri; + if (Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2012")) { + resolveUtcTimingElementDirect(timingElement); + } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2014")) { + resolveUtcTimingElementHttp(timingElement, new Iso8601Parser()); + } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012") + || Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014")) { + resolveUtcTimingElementHttp(timingElement, new XsDateTimeParser()); + } else { + // Unsupported scheme. + onUtcTimestampLoadError(new IOException("Unsupported UTC timing scheme")); + } + } + + private void resolveUtcTimingElementDirect(UtcTimingElement timingElement) { + try { + long utcTimestamp = Util.parseXsDateTime(timingElement.value); + long elapsedRealtimeOffset = utcTimestamp - manifestLoadTimestamp; + onUtcTimestampLoadCompleted(elapsedRealtimeOffset); + } catch (ParseException e) { + onUtcTimestampLoadError(new ParserException(e)); + } + } + + private void resolveUtcTimingElementHttp(UtcTimingElement timingElement, + UriLoadable.Parser parser) { + loader.startLoading(new UriLoadable<>(Uri.parse(timingElement.value), dataSource, parser), + new UtcTimestampCallback(), 1); + } + private void buildTrackGroups(MediaPresentationDescription manifest) { Period period = manifest.getPeriod(0); int trackGroupCount = 0; @@ -321,4 +356,78 @@ public final class DashSampleSource implements SampleSource, UtcTimingCallback { return Pair.create(chunkSource, trackStream); } + + private final class ManifestCallback implements + Loader.Callback> { + + @Override + public void onLoadCompleted(UriLoadable loadable, + long elapsedMs) { + onManifestLoadCompleted(loadable.getResult()); + } + + @Override + public void onLoadCanceled(UriLoadable loadable, long elapsedMs) { + // Do nothing. + } + + @Override + public int onLoadError(UriLoadable loadable, long elapsedMs, + IOException exception) { + return (exception instanceof ParserException) ? Loader.DONT_RETRY_FATAL : Loader.RETRY; + } + + } + + private final class UtcTimestampCallback implements Loader.Callback> { + + @Override + public void onLoadCompleted(UriLoadable loadable, long elapsedMs) { + onUtcTimestampLoadCompleted(loadable.getResult() - SystemClock.elapsedRealtime()); + } + + @Override + public void onLoadCanceled(UriLoadable loadable, long elapsedMs) { + // Do nothing. + } + + @Override + public int onLoadError(UriLoadable loadable, long elapsedMs, IOException exception) { + onUtcTimestampLoadError(exception); + return Loader.DONT_RETRY; + } + + } + + private static final class XsDateTimeParser implements UriLoadable.Parser { + + @Override + public Long parse(Uri uri, InputStream inputStream) throws IOException { + String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); + try { + return Util.parseXsDateTime(firstLine); + } catch (ParseException e) { + throw new ParserException(e); + } + } + + } + + private static final class Iso8601Parser implements UriLoadable.Parser { + + @Override + public Long parse(Uri uri, InputStream inputStream) throws IOException { + String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); + try { + // TODO: It may be necessary to handle timestamp offsets from UTC. + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + return format.parse(firstLine).getTime(); + } catch (ParseException e) { + throw new ParserException(e); + } + } + + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java index 455d6b7732..a2d66215e7 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer.dash.mpd; -import com.google.android.exoplayer.util.ManifestFetcher.RedirectingManifest; - import android.net.Uri; import java.util.Collections; @@ -25,7 +23,7 @@ import java.util.List; /** * Represents a DASH media presentation description (mpd). */ -public class MediaPresentationDescription implements RedirectingManifest { +public class MediaPresentationDescription { public final long availabilityStartTime; @@ -59,11 +57,6 @@ public class MediaPresentationDescription implements RedirectingManifest { this.periods = periods == null ? Collections.emptyList() : periods; } - @Override - public final Uri getNextManifestUri() { - return location; - } - public final int getPeriodCount() { return periods.size(); } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/UtcTimingElementResolver.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/UtcTimingElementResolver.java deleted file mode 100644 index 025384e6c3..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/UtcTimingElementResolver.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.dash.mpd; - -import com.google.android.exoplayer.ParserException; -import com.google.android.exoplayer.upstream.DataSource; -import com.google.android.exoplayer.upstream.Loader; -import com.google.android.exoplayer.upstream.UriLoadable; -import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.Util; - -import android.net.Uri; -import android.os.SystemClock; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Locale; -import java.util.TimeZone; -import java.util.concurrent.CancellationException; - -/** - * Resolves a {@link UtcTimingElement}. - */ -public final class UtcTimingElementResolver implements Loader.Callback> { - - /** - * Callback for timing element resolution. - */ - public interface UtcTimingCallback { - - /** - * Invoked when the element has been resolved. - * - * @param utcTiming The element that was resolved. - * @param elapsedRealtimeOffset The offset between the resolved UTC time and - * {@link SystemClock#elapsedRealtime()} in milliseconds, specified as the UTC time minus - * the local elapsed time. - */ - void onTimestampResolved(UtcTimingElement utcTiming, long elapsedRealtimeOffset); - - /** - * Invoked when the element was not successfully resolved. - * - * @param utcTiming The element that was not resolved. - * @param e The cause of the failure. - */ - void onTimestampError(UtcTimingElement utcTiming, IOException e); - } - - private final DataSource dataSource; - private final UtcTimingElement timingElement; - private final long timingElementElapsedRealtime; - private final UtcTimingCallback callback; - - private Loader singleUseLoader; - - /** - * Resolves a {@link UtcTimingElement}. - * - * @param dataSource A {@link DataSource} to use should loading from a {@link Uri} be necessary. - * @param timingElement The element to resolve. - * @param timingElementElapsedRealtime The {@link SystemClock#elapsedRealtime()} timestamp at - * which the element was obtained. Used if the element contains a timestamp directly. - * @param callback The callback to invoke on resolution or failure. - */ - public static void resolveTimingElement(DataSource dataSource, UtcTimingElement timingElement, - long timingElementElapsedRealtime, UtcTimingCallback callback) { - UtcTimingElementResolver resolver = new UtcTimingElementResolver(dataSource, timingElement, - timingElementElapsedRealtime, callback); - resolver.resolve(); - } - - private UtcTimingElementResolver(DataSource dataSource, UtcTimingElement timingElement, - long timingElementElapsedRealtime, UtcTimingCallback callback) { - this.dataSource = dataSource; - this.timingElement = Assertions.checkNotNull(timingElement); - this.timingElementElapsedRealtime = timingElementElapsedRealtime; - this.callback = Assertions.checkNotNull(callback); - } - - private void resolve() { - String scheme = timingElement.schemeIdUri; - if (Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2012")) { - resolveDirect(); - } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2014")) { - resolveHttp(new Iso8601Parser()); - } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012") - || Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014")) { - resolveHttp(new XsDateTimeParser()); - } else { - // Unsupported scheme. - callback.onTimestampError(timingElement, new IOException("Unsupported utc timing scheme")); - } - } - - private void resolveDirect() { - try { - long utcTimestamp = Util.parseXsDateTime(timingElement.value); - long elapsedRealtimeOffset = utcTimestamp - timingElementElapsedRealtime; - callback.onTimestampResolved(timingElement, elapsedRealtimeOffset); - } catch (ParseException e) { - callback.onTimestampError(timingElement, new ParserException(e)); - } - } - - private void resolveHttp(UriLoadable.Parser parser) { - singleUseLoader = new Loader("Loader:UtcTiming", 0); - singleUseLoader.startLoading( - new UriLoadable<>(Uri.parse(timingElement.value), dataSource, parser), this); - } - - @Override - public void onLoadCompleted(UriLoadable loadable, long elapsedMs) { - releaseLoader(); - long elapsedRealtimeOffset = loadable.getResult() - SystemClock.elapsedRealtime(); - callback.onTimestampResolved(timingElement, elapsedRealtimeOffset); - } - - @Override - public void onLoadCanceled(UriLoadable loadable, long elapsedMs) { - onLoadError(loadable, elapsedMs, - new IOException("Load cancelled", new CancellationException())); - } - - @Override - public int onLoadError(UriLoadable loadable, long elapsedMs, IOException exception) { - releaseLoader(); - callback.onTimestampError(timingElement, exception); - return Loader.DONT_RETRY; - } - - private void releaseLoader() { - singleUseLoader.release(); - } - - private static class XsDateTimeParser implements UriLoadable.Parser { - - @Override - public Long parse(Uri uri, InputStream inputStream) throws IOException { - String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); - try { - return Util.parseXsDateTime(firstLine); - } catch (ParseException e) { - throw new ParserException(e); - } - } - - } - - private static class Iso8601Parser implements UriLoadable.Parser { - - @Override - public Long parse(Uri uri, InputStream inputStream) throws IOException { - String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); - try { - // TODO: It may be necessary to handle timestamp offsets from UTC. - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); - format.setTimeZone(TimeZone.getTimeZone("UTC")); - return format.parse(firstLine).getTime(); - } catch (ParseException e) { - throw new ParserException(e); - } - } - - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java index a0fe9a2464..c6a9eb98b5 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java @@ -231,10 +231,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu this.allocator = allocator; this.requestedBufferSize = requestedBufferSize; this.minLoadableRetryCount = minLoadableRetryCount; - // Assume on-demand until we know otherwise. - int initialMinRetryCount = minLoadableRetryCount == MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA - ? DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND : minLoadableRetryCount; - loader = new Loader("Loader:ExtractorSampleSource", initialMinRetryCount); + loader = new Loader("Loader:ExtractorSampleSource"); extractorHolder = new ExtractorHolder(extractors, this); pendingResetPositionUs = C.UNSET_TIME_US; sampleQueues = new DefaultTrackOutput[0]; @@ -363,10 +360,6 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu trackArray[i] = new TrackGroup(sampleQueues[i].getUpstreamFormat()); } tracks = new TrackGroupArray(trackArray); - if (minLoadableRetryCount == MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA && !seekMap.isSeekable() - && durationUs == C.UNSET_TIME_US) { - loader.setMinRetryCount(DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE); - } prepared = true; return true; } @@ -594,7 +587,16 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu pendingResetPositionUs = C.UNSET_TIME_US; } extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); - loader.startLoading(loadable, this); + + int minRetryCount = minLoadableRetryCount; + if (minRetryCount == MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA) { + // We assume on-demand before we're prepared. + minRetryCount = !prepared || (length != C.LENGTH_UNBOUNDED + || (seekMap != null && seekMap.getDurationUs() != C.UNSET_TIME_US)) + ? DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND + : DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE; + } + loader.startLoading(loadable, this, minRetryCount); } private void configureRetry() { diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java index b195296fc3..6a1b8d7043 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer.C; import com.google.android.exoplayer.DefaultLoadControl; import com.google.android.exoplayer.Format; import com.google.android.exoplayer.LoadControl; +import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackGroup; import com.google.android.exoplayer.TrackGroupArray; @@ -35,7 +36,8 @@ import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSourceFactory; import com.google.android.exoplayer.upstream.DefaultAllocator; -import com.google.android.exoplayer.util.ManifestFetcher; +import com.google.android.exoplayer.upstream.Loader; +import com.google.android.exoplayer.upstream.UriLoadable; import com.google.android.exoplayer.util.MimeTypes; import android.net.Uri; @@ -51,14 +53,15 @@ import java.util.List; /** * A {@link SampleSource} for HLS streams. */ -public final class HlsSampleSource implements SampleSource { +public final class HlsSampleSource implements SampleSource, + Loader.Callback> { /** * The minimum number of times to retry loading data prior to failing. */ - // TODO: Use this for playlist loads as well. private static final int MIN_LOADABLE_RETRY_COUNT = 3; + private final Uri manifestUri; private final DataSourceFactory dataSourceFactory; private final BandwidthMeter bandwidthMeter; private final Handler eventHandler; @@ -66,7 +69,9 @@ public final class HlsSampleSource implements SampleSource { private final LoadControl loadControl; private final IdentityHashMap trackStreamSources; private final PtsTimestampAdjusterProvider timestampAdjusterProvider; - private final ManifestFetcher manifestFetcher; + private final Loader manifestFetcher; + private final DataSource manifestDataSource; + private final HlsPlaylistParser manifestParser; private boolean seenFirstTrackSelection; private long durationUs; @@ -78,9 +83,10 @@ public final class HlsSampleSource implements SampleSource { private boolean pendingReset; private long lastSeekPositionUs; - public HlsSampleSource(Uri uri, DataSourceFactory dataSourceFactory, + public HlsSampleSource(Uri manifestUri, DataSourceFactory dataSourceFactory, BandwidthMeter bandwidthMeter, Handler eventHandler, ChunkTrackStreamEventListener eventListener) { + this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.bandwidthMeter = bandwidthMeter; this.eventHandler = eventHandler; @@ -90,9 +96,9 @@ public final class HlsSampleSource implements SampleSource { timestampAdjusterProvider = new PtsTimestampAdjusterProvider(); trackStreamSources = new IdentityHashMap<>(); - HlsPlaylistParser parser = new HlsPlaylistParser(); - DataSource manifestDataSource = dataSourceFactory.createDataSource(); - manifestFetcher = new ManifestFetcher<>(uri, manifestDataSource, parser); + manifestDataSource = dataSourceFactory.createDataSource(); + manifestParser = new HlsPlaylistParser(); + manifestFetcher = new Loader("Loader:ManifestFetcher"); } @Override @@ -102,16 +108,13 @@ public final class HlsSampleSource implements SampleSource { } if (trackStreamWrappers == null) { - HlsPlaylist playlist = manifestFetcher.getManifest(); - if (playlist == null) { - manifestFetcher.maybeThrowError(); - manifestFetcher.requestRefresh(); - return false; + manifestFetcher.maybeThrowError(); + if (!manifestFetcher.isLoading()) { + manifestFetcher.startLoading( + new UriLoadable<>(manifestUri, manifestDataSource, manifestParser), this, + MIN_LOADABLE_RETRY_COUNT); } - List trackStreamWrapperList = buildTrackStreamWrappers(playlist); - trackStreamWrappers = new HlsTrackStreamWrapper[trackStreamWrapperList.size()]; - trackStreamWrapperList.toArray(trackStreamWrappers); - selectedTrackCounts = new int[trackStreamWrappers.length]; + return false; } boolean trackStreamWrappersPrepared = true; @@ -231,6 +234,27 @@ public final class HlsSampleSource implements SampleSource { } } + // Loader.Callback implementation. + + @Override + public void onLoadCompleted(UriLoadable loadable, long elapsedMs) { + HlsPlaylist playlist = loadable.getResult(); + List trackStreamWrapperList = buildTrackStreamWrappers(playlist); + trackStreamWrappers = new HlsTrackStreamWrapper[trackStreamWrapperList.size()]; + trackStreamWrapperList.toArray(trackStreamWrappers); + selectedTrackCounts = new int[trackStreamWrappers.length]; + } + + @Override + public void onLoadCanceled(UriLoadable loadable, long elapsedMs) { + // Do nothing. + } + + @Override + public int onLoadError(UriLoadable loadable, long elapsedMs, IOException exception) { + return exception instanceof ParserException ? Loader.DONT_RETRY_FATAL : Loader.RETRY; + } + // Internal methods. private List buildTrackStreamWrappers(HlsPlaylist playlist) { diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsTrackStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsTrackStreamWrapper.java index 93b20b0bc1..efdb53273a 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsTrackStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsTrackStreamWrapper.java @@ -54,16 +54,17 @@ import java.util.List; private static final int PRIMARY_TYPE_AUDIO = 2; private static final int PRIMARY_TYPE_VIDEO = 3; - private final Loader loader; private final HlsChunkSource chunkSource; - private final SparseArray sampleQueues; - private final LinkedList mediaChunks; - private final int bufferSizeContribution; - private final ChunkHolder nextChunkHolder; - private final EventDispatcher eventDispatcher; private final LoadControl loadControl; + private final int bufferSizeContribution; private final Format muxedAudioFormat; private final Format muxedCaptionFormat; + private final int minLoadableRetryCount; + private final Loader loader; + private final EventDispatcher eventDispatcher; + private final ChunkHolder nextChunkHolder; + private final SparseArray sampleQueues; + private final LinkedList mediaChunks; private volatile boolean sampleQueuesBuilt; @@ -110,7 +111,8 @@ import java.util.List; this.bufferSizeContribution = bufferSizeContribution; this.muxedAudioFormat = muxedAudioFormat; this.muxedCaptionFormat = muxedCaptionFormat; - loader = new Loader("Loader:HlsTrackStreamWrapper", minLoadableRetryCount); + this.minLoadableRetryCount = minLoadableRetryCount; + loader = new Loader("Loader:HlsTrackStreamWrapper"); eventDispatcher = new EventDispatcher(eventHandler, eventListener, eventSourceId); nextChunkHolder = new ChunkHolder(); sampleQueues = new SparseArray<>(); @@ -575,7 +577,7 @@ import java.util.List; eventDispatcher.loadStarted(loadable.dataSpec.length, loadable.type, loadable.trigger, loadable.format, -1, -1); } - loader.startLoading(loadable, this); + loader.startLoading(loadable, this, minLoadableRetryCount); if (prepared) { // Update the load control again to indicate that we're now loading. loadControl.update(this, downstreamPositionUs, getNextLoadPositionUs(), true); diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingSampleSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingSampleSource.java index 81968b64c9..36b1d2fbd8 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingSampleSource.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer.C; import com.google.android.exoplayer.DefaultLoadControl; import com.google.android.exoplayer.Format; import com.google.android.exoplayer.LoadControl; +import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackGroup; import com.google.android.exoplayer.TrackGroupArray; @@ -35,7 +36,8 @@ import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSourceFactory; import com.google.android.exoplayer.upstream.DefaultAllocator; -import com.google.android.exoplayer.util.ManifestFetcher; +import com.google.android.exoplayer.upstream.Loader; +import com.google.android.exoplayer.upstream.UriLoadable; import com.google.android.exoplayer.util.Util; import android.net.Uri; @@ -51,26 +53,32 @@ import java.util.List; /** * A {@link SampleSource} for SmoothStreaming media. */ -public final class SmoothStreamingSampleSource implements SampleSource { +public final class SmoothStreamingSampleSource implements SampleSource, + Loader.Callback> { /** * The minimum number of times to retry loading data prior to failing. */ - // TODO: Use this for manifest loads as well. private static final int MIN_LOADABLE_RETRY_COUNT = 3; private static final int MINIMUM_MANIFEST_REFRESH_PERIOD_MS = 5000; private static final int INITIALIZATION_VECTOR_SIZE = 8; + private final Uri manifestUri; private final DataSourceFactory dataSourceFactory; private final BandwidthMeter bandwidthMeter; private final Handler eventHandler; private final ChunkTrackStreamEventListener eventListener; private final LoadControl loadControl; - private final ManifestFetcher manifestFetcher; + private final Loader manifestLoader; + private final DataSource manifestDataSource; + private final SmoothStreamingManifestParser manifestParser; + private long manifestLoadTimestamp; + private SmoothStreamingManifest manifest; + + private boolean prepared; private long durationUs; - private SmoothStreamingManifest currentManifest; private TrackEncryptionBox[] trackEncryptionBoxes; private TrackGroupArray trackGroups; private int[] trackGroupElementIndices; @@ -80,9 +88,11 @@ public final class SmoothStreamingSampleSource implements SampleSource { private SmoothStreamingChunkSource[] chunkSources; private ChunkTrackStream[] trackStreams; - public SmoothStreamingSampleSource(Uri uri, DataSourceFactory dataSourceFactory, + public SmoothStreamingSampleSource(Uri manifestUri, DataSourceFactory dataSourceFactory, BandwidthMeter bandwidthMeter, Handler eventHandler, ChunkTrackStreamEventListener eventListener) { + this.manifestUri = Util.toLowerInvariant(manifestUri.getLastPathSegment()).equals("manifest") + ? manifestUri : Uri.withAppendedPath(manifestUri, "Manifest"); this.dataSourceFactory = dataSourceFactory; this.bandwidthMeter = bandwidthMeter; this.eventHandler = eventHandler; @@ -92,37 +102,21 @@ public final class SmoothStreamingSampleSource implements SampleSource { chunkSources = new SmoothStreamingChunkSource[0]; trackStreams = new ChunkTrackStream[0]; - if (!Util.toLowerInvariant(uri.getLastPathSegment()).equals("manifest")) { - uri = Uri.withAppendedPath(uri, "Manifest"); - } - SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); - DataSource manifestDataSource = dataSourceFactory.createDataSource(); - manifestFetcher = new ManifestFetcher<>(uri, manifestDataSource, parser); + manifestDataSource = dataSourceFactory.createDataSource(); + manifestParser = new SmoothStreamingManifestParser(); + manifestLoader = new Loader("Loader:Manifest"); } @Override public boolean prepare(long positionUs) throws IOException { - if (currentManifest != null) { - // Already prepared. + if (prepared) { return true; } - - currentManifest = manifestFetcher.getManifest(); - if (currentManifest == null) { - manifestFetcher.maybeThrowError(); - manifestFetcher.requestRefresh(); - return false; + manifestLoader.maybeThrowError(); + if (!manifestLoader.isLoading()) { + startLoadingManifest(); } - - durationUs = currentManifest.durationUs; - buildTrackGroups(currentManifest); - ProtectionElement protectionElement = currentManifest.protectionElement; - if (protectionElement != null) { - byte[] keyId = getProtectionElementKeyId(protectionElement.data); - trackEncryptionBoxes = new TrackEncryptionBox[] { - new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId)}; - } - return true; + return false; } @Override @@ -173,26 +167,17 @@ 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) { + if (manifest.isLive) { + if (!manifestLoader.isLoading() && SystemClock.elapsedRealtime() + > manifestLoadTimestamp + MINIMUM_MANIFEST_REFRESH_PERIOD_MS) { for (SmoothStreamingChunkSource chunkSource : chunkSources) { if (chunkSource.needManifestRefresh()) { - manifestFetcher.requestRefresh(); + startLoadingManifest(); break; } } } } - for (ChunkTrackStream trackStream : trackStreams) { trackStream.continueBuffering(positionUs); } @@ -234,7 +219,7 @@ public final class SmoothStreamingSampleSource implements SampleSource { @Override public void release() { - manifestFetcher.release(); + manifestLoader.release(); for (SmoothStreamingChunkSource chunkSource : chunkSources) { chunkSource.release(); } @@ -243,7 +228,46 @@ public final class SmoothStreamingSampleSource implements SampleSource { } } - // Internal methods. + // Loader.Callback implementation + + @Override + public void onLoadCompleted(UriLoadable loadable, long elapsedMs) { + manifest = loadable.getResult(); + manifestLoadTimestamp = SystemClock.elapsedRealtime(); + if (!prepared) { + durationUs = manifest.durationUs; + buildTrackGroups(manifest); + ProtectionElement protectionElement = manifest.protectionElement; + if (protectionElement != null) { + byte[] keyId = getProtectionElementKeyId(protectionElement.data); + trackEncryptionBoxes = new TrackEncryptionBox[] { + new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId)}; + } + prepared = true; + } else { + for (SmoothStreamingChunkSource chunkSource : chunkSources) { + chunkSource.updateManifest(manifest); + } + } + } + + @Override + public void onLoadCanceled(UriLoadable loadable, long elapsedMs) { + // Do nothing. + } + + @Override + public int onLoadError(UriLoadable loadable, long elapsedMs, + IOException exception) { + return (exception instanceof ParserException) ? Loader.DONT_RETRY_FATAL : Loader.RETRY; + } + + // Internal methods + + private void startLoadingManifest() { + manifestLoader.startLoading(new UriLoadable<>(manifestUri, manifestDataSource, manifestParser), + this, MIN_LOADABLE_RETRY_COUNT); + } private void buildTrackGroups(SmoothStreamingManifest manifest) { int trackGroupCount = 0; @@ -273,11 +297,11 @@ public final class SmoothStreamingSampleSource implements SampleSource { FormatEvaluator adaptiveEvaluator = selectedTracks.length > 1 ? new AdaptiveEvaluator(bandwidthMeter) : null; int streamElementIndex = trackGroupElementIndices[selection.group]; - StreamElement streamElement = currentManifest.streamElements[streamElementIndex]; + StreamElement streamElement = manifest.streamElements[streamElementIndex]; int streamElementType = streamElement.type; int bufferSize = Util.getDefaultBufferSize(streamElementType); DataSource dataSource = dataSourceFactory.createDataSource(bandwidthMeter); - SmoothStreamingChunkSource chunkSource = new SmoothStreamingChunkSource(currentManifest, + SmoothStreamingChunkSource chunkSource = new SmoothStreamingChunkSource(manifest, streamElementIndex, trackGroups.get(selection.group), selectedTracks, dataSource, adaptiveEvaluator, trackEncryptionBoxes); ChunkTrackStream trackStream = new ChunkTrackStream(chunkSource, loadControl, bufferSize, diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java b/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java index a1356a5c8e..9a5515d98d 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java @@ -120,17 +120,14 @@ public final class Loader { private final ExecutorService downloadExecutorService; - private int minRetryCount; private LoadTask currentTask; private IOException fatalError; /** * @param threadName A name for the loader's thread. - * @param minRetryCount The minimum retry count. */ - public Loader(String threadName, int minRetryCount) { + public Loader(String threadName) { this.downloadExecutorService = Util.newSingleThreadExecutor(threadName); - this.minRetryCount = minRetryCount; } /** @@ -141,12 +138,15 @@ public final class Loader { * * @param loadable The {@link Loadable} to load. * @param callback A callback to invoke when the load ends. + * @param minRetryCount The minimum number of times the load must be retried before + * {@link #maybeThrowError()} will propagate an error. * @throws IllegalStateException If the calling thread does not have an associated {@link Looper}. */ - public void startLoading(T loadable, Callback callback) { + public void startLoading(T loadable, Callback callback, + int minRetryCount) { Looper looper = Looper.myLooper(); Assertions.checkState(looper != null); - new LoadTask<>(looper, loadable, callback).start(0); + new LoadTask<>(looper, loadable, callback, minRetryCount).start(0); } /** @@ -158,18 +158,9 @@ public final class Loader { return currentTask != null; } - /** - * Sets the minimum retry count. - * - * @param minRetryCount The minimum retry count. - */ - public void setMinRetryCount(int minRetryCount) { - this.minRetryCount = minRetryCount; - } - /** * If a fatal error has been encountered, or if the current {@link Loadable} has incurred a number - * of errors greater than the minimum number of retries and if the load is currently backed off, + * of errors greater than its minimum number of retries and if the load is currently backed off, * then an error is thrown. Else does nothing. * * @throws IOException The error. @@ -178,7 +169,7 @@ public final class Loader { if (fatalError != null) { throw fatalError; } else if (currentTask != null) { - currentTask.maybeThrowError(minRetryCount); + currentTask.maybeThrowError(); } } @@ -211,20 +202,22 @@ public final class Loader { private final T loadable; private final Loader.Callback callback; private final long startTimeMs; + private final int minRetryCount; private IOException currentError; private int errorCount; private volatile Thread executorThread; - public LoadTask(Looper looper, T loadable, Loader.Callback callback) { + public LoadTask(Looper looper, T loadable, Loader.Callback callback, int minRetryCount) { super(looper); this.loadable = loadable; this.callback = callback; + this.minRetryCount = minRetryCount; this.startTimeMs = SystemClock.elapsedRealtime(); } - public void maybeThrowError(int minRetryCount) throws IOException { + public void maybeThrowError() throws IOException { if (currentError != null && errorCount > minRetryCount) { throw currentError; } diff --git a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java deleted file mode 100644 index af20cb70c8..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Copyright (C) 2014 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.exoplayer.util; - -import com.google.android.exoplayer.upstream.DataSource; -import com.google.android.exoplayer.upstream.Loader; -import com.google.android.exoplayer.upstream.UriLoadable; - -import android.net.Uri; -import android.os.Handler; -import android.os.SystemClock; -import android.util.Pair; - -import java.io.IOException; - -/** - * Loads and refreshes a media manifest. - * - * @param The type of manifest. - */ -public class ManifestFetcher implements Loader.Callback> { - - /** - * Thrown when an error occurs trying to fetch a manifest. - */ - public static final class ManifestIOException extends IOException{ - public ManifestIOException(Throwable cause) { super(cause); } - - } - - /** - * Interface definition for a callback to be notified of {@link ManifestFetcher} events. - */ - public interface EventListener { - - void onManifestRefreshStarted(); - - void onManifestRefreshed(); - - void onManifestError(IOException e); - - } - - /** - * Interface for manifests that are able to specify that subsequent loads should use a different - * URI. - */ - public interface RedirectingManifest { - - /** - * Returns the {@link Uri} from which subsequent manifests should be requested, or null to - * continue using the current {@link Uri}. - */ - Uri getNextManifestUri(); - - } - - private final Loader loader; - private final UriLoadable.Parser parser; - private final DataSource dataSource; - private final Handler eventHandler; - private final EventListener eventListener; - - private volatile Uri manifestUri; - - private long currentLoadStartTimestamp; - - private volatile T manifest; - private volatile long manifestLoadStartTimestamp; - private volatile long manifestLoadCompleteTimestamp; - - /** - * @param manifestUri The manifest {@link Uri}. - * @param dataSource The {@link DataSource} to use when loading the manifest. - * @param parser A parser to parse the loaded manifest data. - */ - public ManifestFetcher(Uri manifestUri, DataSource dataSource, UriLoadable.Parser parser) { - this(manifestUri, dataSource, parser, null, null); - } - - /** - * @param manifestUri The manifest {@link Uri}. - * @param dataSource The {@link DataSource} to use when loading the manifest. - * @param parser A parser to parse the loaded manifest data. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - */ - public ManifestFetcher(Uri manifestUri, DataSource dataSource, UriLoadable.Parser parser, - Handler eventHandler, EventListener eventListener) { - this.parser = parser; - this.manifestUri = manifestUri; - this.dataSource = dataSource; - this.eventHandler = eventHandler; - this.eventListener = eventListener; - loader = new Loader("Loader:ManifestFetcher", 1); - } - - /** - * Updates the manifest {@link Uri}. - * - * @param manifestUri The manifest {@link Uri}. - */ - public void updateManifestUri(Uri manifestUri) { - this.manifestUri = manifestUri; - } - - /** - * Gets a {@link Pair} containing the most recently loaded manifest together with the timestamp - * at which the load completed. - * - * @return The most recently loaded manifest and the timestamp at which the load completed, or - * null if no manifest has loaded. - */ - public T getManifest() { - return manifest; - } - - /** - * Gets the value of {@link SystemClock#elapsedRealtime()} when the last completed load started. - * - * @return The value of {@link SystemClock#elapsedRealtime()} when the last completed load - * started. - */ - public long getManifestLoadStartTimestamp() { - return manifestLoadStartTimestamp; - } - - /** - * Gets the value of {@link SystemClock#elapsedRealtime()} when the last load completed. - * - * @return The value of {@link SystemClock#elapsedRealtime()} when the last load completed. - */ - public long getManifestLoadCompleteTimestamp() { - return manifestLoadCompleteTimestamp; - } - - /** - * Throws the error that affected the most recent attempt to load the manifest. Does nothing if - * the most recent attempt was successful. - * - * @throws ManifestIOException The error that affected the most recent attempt to load the - * manifest. - */ - public void maybeThrowError() throws ManifestIOException { - if (loader != null) { - try { - loader.maybeThrowError(); - } catch (IOException e) { - throw new ManifestIOException(e); - } - } - } - - /** - * Should be invoked repeatedly by callers who require an updated manifest. - */ - public void requestRefresh() { - if (loader.isLoading()) { - return; - } - currentLoadStartTimestamp = SystemClock.elapsedRealtime(); - loader.startLoading(new UriLoadable<>(manifestUri, dataSource, parser), this); - notifyManifestRefreshStarted(); - } - - /** - * Releases the fetcher. - *

- * This method should be called when the fetcher is no longer required. - */ - public void release() { - loader.release(); - } - - // Loadable.Callback implementation. - - @Override - public void onLoadCompleted(UriLoadable loadable, long elapsedMs) { - manifest = loadable.getResult(); - manifestLoadStartTimestamp = currentLoadStartTimestamp; - manifestLoadCompleteTimestamp = SystemClock.elapsedRealtime(); - if (manifest instanceof RedirectingManifest) { - RedirectingManifest redirectingManifest = (RedirectingManifest) manifest; - Uri nextUri = redirectingManifest.getNextManifestUri(); - if (nextUri != null) { - manifestUri = nextUri; - } - } - notifyManifestRefreshed(); - } - - @Override - public void onLoadCanceled(UriLoadable loadable, long elapsedMs) { - // Do nothing. - } - - @Override - public int onLoadError(UriLoadable loadable, long elapsedMs, IOException exception) { - notifyManifestError(new ManifestIOException(exception)); - return Loader.RETRY; - } - - // Private methods. - - private void notifyManifestRefreshStarted() { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onManifestRefreshStarted(); - } - }); - } - } - - private void notifyManifestRefreshed() { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onManifestRefreshed(); - } - }); - } - } - - private void notifyManifestError(final IOException e) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onManifestError(e); - } - }); - } - } - -}