/*
* 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.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.upstream.UriDataSource;
import com.google.android.exoplayer.upstream.UriLoadable;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Pair;
import java.io.IOException;
import java.util.concurrent.CancellationException;
/**
* Performs both single and repeated loads of media manifests.
* <p>
* Client code is responsible for ensuring that only one load is taking place at any one time.
* Typical usage of this class is as follows:
* <ol>
* <li>Create an instance.</li>
* <li>Obtain an initial manifest by calling {@link #singleLoad(Looper, ManifestCallback)} and
* waiting for the callback to be invoked.</li>
* <li>For on-demand playbacks, the loader is no longer required. For live playbacks, the loader
* may be required to periodically refresh the manifest. In this case it is injected into any
* components that require it. These components will call {@link #requestRefresh()} on the
* loader whenever a refresh is required.</li>
* </ol>
*
* @param <T> The type of manifest.
*/
public class ManifestFetcher<T> implements Loader.Callback {
/**
* Interface definition for a callback to be notified of {@link ManifestFetcher} events.
*/
public interface EventListener {
public void onManifestRefreshStarted();
public void onManifestRefreshed();
public void onManifestError(IOException e);
}
/**
* Callback for the result of a single load.
*
* @param <T> The type of manifest.
*/
public interface ManifestCallback<T> {
/**
* Invoked when the load has successfully completed.
*
* @param manifest The loaded manifest.
*/
void onSingleManifest(T manifest);
/**
* Invoked when the load has failed.
*
* @param e The cause of the failure.
*/
void onSingleManifestError(IOException e);
}
/**
* Interface for manifests that are able to specify that subsequent loads should use a different
* URI.
*/
public interface RedirectingManifest {
/**
* Returns the URI from which subsequent manifests should be requested, or null to continue
* using the current URI.
*/
public String getNextManifestUri();
}
private final UriLoadable.Parser<T> parser;
private final UriDataSource uriDataSource;
private final Handler eventHandler;
private final EventListener eventListener;
/* package */ volatile String manifestUri;
private int enabledCount;
private Loader loader;
private UriLoadable<T> currentLoadable;
private long currentLoadStartTimestamp;
private int loadExceptionCount;
private long loadExceptionTimestamp;
private IOException loadException;
private volatile T manifest;
private volatile long manifestLoadStartTimestamp;
private volatile long manifestLoadCompleteTimestamp;
/**
* @param manifestUri The manifest location.
* @param uriDataSource The {@link UriDataSource} to use when loading the manifest.
* @param parser A parser to parse the loaded manifest data.
*/
public ManifestFetcher(String manifestUri, UriDataSource uriDataSource,
UriLoadable.Parser<T> parser) {
this(manifestUri, uriDataSource, parser, null, null);
}
/**
* @param manifestUri The manifest location.
* @param uriDataSource The {@link UriDataSource} 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(String manifestUri, UriDataSource uriDataSource,
UriLoadable.Parser<T> parser, Handler eventHandler, EventListener eventListener) {
this.parser = parser;
this.manifestUri = manifestUri;
this.uriDataSource = uriDataSource;
this.eventHandler = eventHandler;
this.eventListener = eventListener;
}
/**
* Updates the manifest location.
*
* @param manifestUri The manifest location.
*/
public void updateManifestUri(String manifestUri) {
this.manifestUri = manifestUri;
}
/**
* 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<T> callback) {
SingleFetchHelper fetchHelper = new SingleFetchHelper(
new UriLoadable<>(manifestUri, uriDataSource, parser), 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 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 IOException The error that affected the most recent attempt to load the manifest.
*/
public void maybeThrowError() throws IOException {
// Don't throw an exception until at least 1 retry attempt has been made.
if (loadException == null || loadExceptionCount <= 1) {
return;
}
throw 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 UriLoadable<>(manifestUri, uriDataSource, parser);
currentLoadStartTimestamp = SystemClock.elapsedRealtime();
loader.startLoading(currentLoadable, this);
notifyManifestRefreshStarted();
}
}
@Override
public void onLoadCompleted(Loadable loadable) {
if (currentLoadable != loadable) {
// Stale event.
return;
}
manifest = currentLoadable.getResult();
manifestLoadStartTimestamp = currentLoadStartTimestamp;
manifestLoadCompleteTimestamp = SystemClock.elapsedRealtime();
loadExceptionCount = 0;
loadException = null;
if (manifest instanceof RedirectingManifest) {
RedirectingManifest redirectingManifest = (RedirectingManifest) manifest;
String nextLocation = redirectingManifest.getNextManifestUri();
if (!TextUtils.isEmpty(nextLocation)) {
manifestUri = nextLocation;
}
}
notifyManifestRefreshed();
}
@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);
notifyManifestError(loadException);
}
/* package */ void onSingleFetchCompleted(T result, long loadStartTimestamp) {
manifest = result;
manifestLoadStartTimestamp = loadStartTimestamp;
manifestLoadCompleteTimestamp = SystemClock.elapsedRealtime();
}
private long getRetryDelayMillis(long errorCount) {
return Math.min((errorCount - 1) * 1000, 5000);
}
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);
}
});
}
}
private class SingleFetchHelper implements Loader.Callback {
private final UriLoadable<T> singleUseLoadable;
private final Looper callbackLooper;
private final ManifestCallback<T> wrappedCallback;
private final Loader singleUseLoader;
private long loadStartTimestamp;
public SingleFetchHelper(UriLoadable<T> singleUseLoadable, Looper callbackLooper,
ManifestCallback<T> wrappedCallback) {
this.singleUseLoadable = singleUseLoadable;
this.callbackLooper = callbackLooper;
this.wrappedCallback = wrappedCallback;
singleUseLoader = new Loader("manifestLoader:single");
}
public void startLoading() {
loadStartTimestamp = SystemClock.elapsedRealtime();
singleUseLoader.startLoading(callbackLooper, singleUseLoadable, this);
}
@Override
public void onLoadCompleted(Loadable loadable) {
try {
T result = singleUseLoadable.getResult();
onSingleFetchCompleted(result, loadStartTimestamp);
wrappedCallback.onSingleManifest(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.onSingleManifestError(exception);
} finally {
releaseLoader();
}
}
@Override
public void onLoadError(Loadable loadable, IOException exception) {
try {
wrappedCallback.onSingleManifestError(exception);
} finally {
releaseLoader();
}
}
private void releaseLoader() {
singleUseLoader.release();
}
}
}