diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java index 966ce7a43b..32d60beafc 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashVodRendererBuilder.java @@ -52,11 +52,11 @@ import com.google.android.exoplayer.util.Util; import android.annotation.TargetApi; import android.media.MediaCodec; import android.media.UnsupportedSchemeException; -import android.os.AsyncTask; import android.os.Handler; import android.util.Pair; import android.widget.TextView; +import java.io.IOException; import java.util.ArrayList; /** @@ -96,13 +96,13 @@ public class DashVodRendererBuilder implements RendererBuilder, this.player = player; this.callback = callback; MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); - ManifestFetcher mpdFetcher = - new ManifestFetcher(parser, this); - mpdFetcher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, contentId); + ManifestFetcher manifestFetcher = + new ManifestFetcher(parser, contentId, url); + manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); } @Override - public void onManifestError(String contentId, Exception e) { + public void onManifestError(String contentId, IOException e) { callback.onRenderersError(e); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java index af806e9fe7..a5cb7093e4 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java @@ -43,7 +43,6 @@ import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer.upstream.HttpDataSource; import com.google.android.exoplayer.util.ManifestFetcher; -import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import com.google.android.exoplayer.util.Util; import android.annotation.TargetApi; @@ -52,6 +51,7 @@ import android.media.UnsupportedSchemeException; import android.os.Handler; import android.widget.TextView; +import java.io.IOException; import java.util.ArrayList; import java.util.UUID; @@ -59,7 +59,7 @@ import java.util.UUID; * A {@link RendererBuilder} for SmoothStreaming. */ public class SmoothStreamingRendererBuilder implements RendererBuilder, - ManifestCallback { + ManifestFetcher.ManifestCallback { private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int VIDEO_BUFFER_SEGMENTS = 200; @@ -88,16 +88,15 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) { this.player = player; this.callback = callback; - SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); ManifestFetcher manifestFetcher = - new ManifestFetcher(parser, this); - manifestFetcher.execute(url + "/Manifest", contentId); + new ManifestFetcher(parser, contentId, url + "/Manifest"); + manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); } @Override - public void onManifestError(String contentId, Exception e) { - callback.onRenderersError(e); + public void onManifestError(String contentId, IOException exception) { + callback.onRenderersError(exception); } @Override diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java index cc994543b9..4428fba201 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/DashVodRendererBuilder.java @@ -42,9 +42,9 @@ import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import android.media.MediaCodec; -import android.os.AsyncTask; import android.os.Handler; +import java.io.IOException; import java.util.ArrayList; /** @@ -76,13 +76,13 @@ import java.util.ArrayList; public void buildRenderers(RendererBuilderCallback callback) { this.callback = callback; MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); - ManifestFetcher mpdFetcher = - new ManifestFetcher(parser, this); - mpdFetcher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, contentId); + ManifestFetcher manifestFetcher = + new ManifestFetcher(parser, contentId, url); + manifestFetcher.singleLoad(playerActivity.getMainLooper(), this); } @Override - public void onManifestError(String contentId, Exception e) { + public void onManifestError(String contentId, IOException e) { callback.onRenderersError(e); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java index efde2de096..f936b19219 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/simple/SmoothStreamingRendererBuilder.java @@ -42,6 +42,7 @@ import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import android.media.MediaCodec; import android.os.Handler; +import java.io.IOException; import java.util.ArrayList; /** @@ -74,12 +75,12 @@ import java.util.ArrayList; this.callback = callback; SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); ManifestFetcher manifestFetcher = - new ManifestFetcher(parser, this); - manifestFetcher.execute(url + "/Manifest", contentId); + new ManifestFetcher(parser, contentId, url + "/Manifest"); + manifestFetcher.singleLoad(playerActivity.getMainLooper(), this); } @Override - public void onManifestError(String contentId, Exception e) { + public void onManifestError(String contentId, IOException e) { callback.onRenderersError(e); } 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 index 804b1240e7..423b1d0204 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java @@ -15,114 +15,297 @@ */ package com.google.android.exoplayer.util; -import android.net.Uri; -import android.os.AsyncTask; +import com.google.android.exoplayer.upstream.Loader; +import com.google.android.exoplayer.upstream.Loader.Loadable; + +import android.os.Looper; +import android.os.SystemClock; +import android.util.Pair; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; +import java.util.concurrent.CancellationException; /** - * An {@link AsyncTask} for loading and parsing media manifests. + * Performs both single and repeated loads of media manfifests. * - * @param The type of the manifest being parsed. + * @param The type of manifest. */ -public class ManifestFetcher extends AsyncTask { +public class ManifestFetcher implements Loader.Callback { /** - * Invoked with the result of a manifest fetch. + * Callback for the result of a single load. * - * @param The type of the manifest being parsed. + * @param The type of manifest. */ public interface ManifestCallback { /** - * Invoked from {@link #onPostExecute(Object)} with the parsed manifest. + * Invoked when the load has successfully completed. * * @param contentId The content id of the media. - * @param manifest The parsed manifest. + * @param manifest The loaded manifest. */ void onManifest(String contentId, T manifest); /** - * Invoked from {@link #onPostExecute(Object)} if an error occurred. + * Invoked when the load has failed. * * @param contentId The content id of the media. - * @param e The error. + * @param e The cause of the failure. */ - void onManifestError(String contentId, Exception e); + void onManifestError(String contentId, IOException e); } - public static final int DEFAULT_HTTP_TIMEOUT_MILLIS = 8000; + /* package */ final ManifestParser parser; + /* package */ final String manifestUrl; + /* package */ final String contentId; - private final ManifestParser parser; - private final ManifestCallback callback; - private final int timeoutMillis; + private int enabledCount; + private Loader loader; + private ManifestLoadable currentLoadable; - private volatile String contentId; - private volatile Exception exception; + private int loadExceptionCount; + private long loadExceptionTimestamp; + private IOException loadException; + + private volatile T manifest; + private volatile long manifestLoadTimestamp; /** - * @param callback The callback to provide with the parsed manifest (or error). + * @param parser A parser to parse the loaded manifest data. + * @param contentId The content id of the content being loaded. May be null. + * @param manifestUrl The manifest location. */ - public ManifestFetcher(ManifestParser parser, ManifestCallback callback) { - this(parser, callback, DEFAULT_HTTP_TIMEOUT_MILLIS); - } - - /** - * @param parser Parses the manifest from the loaded data. - * @param callback The callback to provide with the parsed manifest (or error). - * @param timeoutMillis The timeout in milliseconds for the connection used to load the data. - */ - public ManifestFetcher(ManifestParser parser, ManifestCallback callback, - int timeoutMillis) { + public ManifestFetcher(ManifestParser parser, String contentId, String manifestUrl) { this.parser = parser; - this.callback = callback; - this.timeoutMillis = timeoutMillis; + this.contentId = contentId; + this.manifestUrl = manifestUrl; + } + + /** + * Performs a single manifest load. + * + * @param callbackLooper The looper associated with the thread on which the callback should be + * invoked. + * @param callback The callback to receive the result. + */ + public void singleLoad(Looper callbackLooper, final ManifestCallback callback) { + SingleFetchHelper fetchHelper = new SingleFetchHelper(callbackLooper, callback); + fetchHelper.startLoading(); + } + + /** + * 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 load completed. + * + * @return The value of {@link SystemClock#elapsedRealtime()} when the last load completed. + */ + public long getManifestLoadTimestamp() { + return manifestLoadTimestamp; + } + + /** + * Gets the error that affected the most recent attempt to load the manifest, or null if the + * most recent attempt was successful. + * + * @return The error, or null if the most recent attempt was successful. + */ + public IOException getError() { + if (loadExceptionCount <= 1) { + // Don't report an exception until at least 1 retry attempt has been made. + return null; + } + return loadException; + } + + /** + * Enables refresh functionality. + */ + public void enable() { + if (enabledCount++ == 0) { + loadExceptionCount = 0; + loadException = null; + } + } + + /** + * Disables refresh functionality. + */ + public void disable() { + if (--enabledCount == 0) { + if (loader != null) { + loader.release(); + loader = null; + } + } + } + + /** + * Should be invoked repeatedly by callers who require an updated manifest. + */ + public void requestRefresh() { + if (loadException != null && SystemClock.elapsedRealtime() + < (loadExceptionTimestamp + getRetryDelayMillis(loadExceptionCount))) { + // The previous load failed, and it's too soon to try again. + return; + } + if (loader == null) { + loader = new Loader("manifestLoader"); + } + if (!loader.isLoading()) { + currentLoadable = new ManifestLoadable(); + loader.startLoading(currentLoadable, this); + } } @Override - protected final T doInBackground(String... data) { - try { - contentId = data.length > 1 ? data[1] : null; - String urlString = data[0]; + public void onLoadCompleted(Loadable loadable) { + if (currentLoadable != loadable) { + // Stale event. + return; + } + + manifest = currentLoadable.result; + manifestLoadTimestamp = SystemClock.elapsedRealtime(); + loadExceptionCount = 0; + loadException = null; + } + + @Override + public void onLoadCanceled(Loadable loadable) { + // Do nothing. + } + + @Override + public void onLoadError(Loadable loadable, IOException exception) { + if (currentLoadable != loadable) { + // Stale event. + return; + } + + loadExceptionCount++; + loadExceptionTimestamp = SystemClock.elapsedRealtime(); + loadException = new IOException(exception); + } + + private long getRetryDelayMillis(long errorCount) { + return Math.min((errorCount - 1) * 1000, 5000); + } + + private class SingleFetchHelper implements Loader.Callback { + + private final Looper callbackLooper; + private final ManifestCallback wrappedCallback; + private final Loader singleUseLoader; + private final ManifestLoadable singleUseLoadable; + + public SingleFetchHelper(Looper callbackLooper, ManifestCallback wrappedCallback) { + this.callbackLooper = callbackLooper; + this.wrappedCallback = wrappedCallback; + singleUseLoader = new Loader("manifestLoader:single"); + singleUseLoadable = new ManifestLoadable(); + } + + public void startLoading() { + singleUseLoader.startLoading(callbackLooper, singleUseLoadable, this); + } + + @Override + public void onLoadCompleted(Loadable loadable) { + try { + manifest = singleUseLoadable.result; + manifestLoadTimestamp = SystemClock.elapsedRealtime(); + wrappedCallback.onManifest(contentId, singleUseLoadable.result); + } finally { + releaseLoader(); + } + } + + @Override + public void onLoadCanceled(Loadable loadable) { + // This shouldn't ever happen, but handle it anyway. + try { + IOException exception = new IOException("Load cancelled", new CancellationException()); + wrappedCallback.onManifestError(contentId, exception); + } finally { + releaseLoader(); + } + } + + @Override + public void onLoadError(Loadable loadable, IOException exception) { + try { + wrappedCallback.onManifestError(contentId, exception); + } finally { + releaseLoader(); + } + } + + private void releaseLoader() { + singleUseLoader.release(); + } + + } + + private class ManifestLoadable implements Loadable { + + private static final int TIMEOUT_MILLIS = 10000; + + /* package */ volatile T result; + private volatile boolean isCanceled; + + @Override + public void cancelLoad() { + // We don't actually cancel anything, but we need to record the cancellation so that + // isLoadCanceled can return the correct value. + isCanceled = true; + } + + @Override + public boolean isLoadCanceled() { + return isCanceled; + } + + @Override + public void load() throws IOException, InterruptedException { String inputEncoding = null; InputStream inputStream = null; try { - Uri baseUri = Util.parseBaseUri(urlString); - HttpURLConnection connection = configureHttpConnection(new URL(urlString)); + HttpURLConnection connection = configureHttpConnection(new URL(manifestUrl)); inputStream = connection.getInputStream(); inputEncoding = connection.getContentEncoding(); - return parser.parse(inputStream, inputEncoding, contentId, baseUri); + result = parser.parse(inputStream, inputEncoding, contentId, + Util.parseBaseUri(manifestUrl)); } finally { if (inputStream != null) { inputStream.close(); } } - } catch (Exception e) { - exception = e; - return null; } - } - @Override - protected final void onPostExecute(T manifest) { - if (exception != null) { - callback.onManifestError(contentId, exception); - } else { - callback.onManifest(contentId, manifest); + private HttpURLConnection configureHttpConnection(URL url) throws IOException { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(TIMEOUT_MILLIS); + connection.setReadTimeout(TIMEOUT_MILLIS); + connection.setDoOutput(false); + connection.connect(); + return connection; } - } - private HttpURLConnection configureHttpConnection(URL url) throws IOException { - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(timeoutMillis); - connection.setReadTimeout(timeoutMillis); - connection.setDoOutput(false); - connection.connect(); - return connection; } }