Implement UTC time synchronization for DASH Live.

Support is provided for the following schemes:

urn:mpeg:dash:utc:direct:2012
urn:mpeg:dash:utc:http-iso:2014
urn:mpeg:dash:utc:http-xsdate:2012
urn:mpeg:dash:utc:http-xsdate:2014
This commit is contained in:
Oliver Woodman 2015-02-20 15:10:25 +00:00
parent 4076b08e4b
commit 6d14fc3330
6 changed files with 389 additions and 60 deletions

View File

@ -37,6 +37,9 @@ import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser;
import com.google.android.exoplayer.dash.mpd.Period; import com.google.android.exoplayer.dash.mpd.Period;
import com.google.android.exoplayer.dash.mpd.Representation; 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.demo.DemoUtil; import com.google.android.exoplayer.demo.DemoUtil;
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder;
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallback; import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallback;
@ -59,6 +62,7 @@ import android.annotation.TargetApi;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.UnsupportedSchemeException; import android.media.UnsupportedSchemeException;
import android.os.Handler; import android.os.Handler;
import android.util.Log;
import android.util.Pair; import android.util.Pair;
import android.widget.TextView; import android.widget.TextView;
@ -70,7 +74,9 @@ import java.util.List;
* A {@link RendererBuilder} for DASH. * A {@link RendererBuilder} for DASH.
*/ */
public class DashRendererBuilder implements RendererBuilder, public class DashRendererBuilder implements RendererBuilder,
ManifestCallback<MediaPresentationDescription> { ManifestCallback<MediaPresentationDescription>, UtcTimingCallback {
private static final String TAG = "DashRendererBuilder";
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int VIDEO_BUFFER_SEGMENTS = 200; private static final int VIDEO_BUFFER_SEGMENTS = 200;
@ -96,6 +102,9 @@ public class DashRendererBuilder implements RendererBuilder,
private RendererBuilderCallback callback; private RendererBuilderCallback callback;
private ManifestFetcher<MediaPresentationDescription> manifestFetcher; private ManifestFetcher<MediaPresentationDescription> manifestFetcher;
private MediaPresentationDescription manifest;
private long elapsedRealtimeOffset;
public DashRendererBuilder(String userAgent, String url, String contentId, public DashRendererBuilder(String userAgent, String url, String contentId,
MediaDrmCallback drmCallback, TextView debugTextView, AudioCapabilities audioCapabilities) { MediaDrmCallback drmCallback, TextView debugTextView, AudioCapabilities audioCapabilities) {
this.userAgent = userAgent; this.userAgent = userAgent;
@ -116,13 +125,36 @@ public class DashRendererBuilder implements RendererBuilder,
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
} }
@Override
public void onManifest(String contentId, MediaPresentationDescription manifest) {
this.manifest = manifest;
if (manifest.dynamic && manifest.utcTiming != null) {
UtcTimingElementResolver.resolveTimingElement(userAgent, manifest.utcTiming,
manifestFetcher.getManifestLoadTimestamp(), this);
} else {
buildRenderers();
}
}
@Override @Override
public void onManifestError(String contentId, IOException e) { public void onManifestError(String contentId, IOException e) {
callback.onRenderersError(e); callback.onRenderersError(e);
} }
@Override @Override
public void onManifest(String contentId, MediaPresentationDescription manifest) { public void onTimestampResolved(UtcTimingElement utcTiming, long elapsedRealtimeOffset) {
this.elapsedRealtimeOffset = elapsedRealtimeOffset;
buildRenderers();
}
@Override
public void onTimestampError(UtcTimingElement utcTiming, IOException e) {
Log.e(TAG, "Failed to resolve UtcTiming element [" + utcTiming + "]", e);
// Be optimistic and continue in the hope that the device clock is correct.
buildRenderers();
}
private void buildRenderers() {
Period period = manifest.periods.get(0); Period period = manifest.periods.get(0);
Handler mainHandler = player.getMainHandler(); Handler mainHandler = player.getMainHandler();
LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE));
@ -207,7 +239,7 @@ public class DashRendererBuilder implements RendererBuilder,
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter); DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex, ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex,
videoRepresentationIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter), videoRepresentationIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter),
LIVE_EDGE_LATENCY_MS); LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
DemoPlayer.TYPE_VIDEO); DemoPlayer.TYPE_VIDEO);
@ -230,7 +262,8 @@ public class DashRendererBuilder implements RendererBuilder,
audioTrackNameList.add(format.id + " (" + format.numChannels + "ch, " + audioTrackNameList.add(format.id + " (" + format.numChannels + "ch, " +
format.audioSamplingRate + "Hz)"); format.audioSamplingRate + "Hz)");
audioChunkSourceList.add(new DashChunkSource(manifestFetcher, audioAdaptationSetIndex, audioChunkSourceList.add(new DashChunkSource(manifestFetcher, audioAdaptationSetIndex,
new int[] {i}, audioDataSource, audioEvaluator, LIVE_EDGE_LATENCY_MS)); new int[] {i}, audioDataSource, audioEvaluator, LIVE_EDGE_LATENCY_MS,
elapsedRealtimeOffset));
haveAc3Tracks |= AC_3_CODEC.equals(format.codecs) || E_AC_3_CODEC.equals(format.codecs); haveAc3Tracks |= AC_3_CODEC.equals(format.codecs) || E_AC_3_CODEC.equals(format.codecs);
} }
// Filter out non-AC-3 tracks if there is an AC-3 track, to avoid having to switch renderers. // Filter out non-AC-3 tracks if there is an AC-3 track, to avoid having to switch renderers.
@ -285,7 +318,7 @@ public class DashRendererBuilder implements RendererBuilder,
Representation representation = representations.get(j); Representation representation = representations.get(j);
textTrackNameList.add(representation.format.id); textTrackNameList.add(representation.format.id);
textChunkSourceList.add(new DashChunkSource(manifestFetcher, i, new int[] {j}, textChunkSourceList.add(new DashChunkSource(manifestFetcher, i, new int[] {j},
textDataSource, textEvaluator, LIVE_EDGE_LATENCY_MS)); textDataSource, textEvaluator, LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset));
} }
} }
} }

View File

@ -86,6 +86,7 @@ public class DashChunkSource implements ChunkSource {
private final Evaluation evaluation; private final Evaluation evaluation;
private final StringBuilder headerBuilder; private final StringBuilder headerBuilder;
private final long liveEdgeLatencyUs; private final long liveEdgeLatencyUs;
private final long elapsedRealtimeOffsetUs;
private final int maxWidth; private final int maxWidth;
private final int maxHeight; private final int maxHeight;
@ -140,7 +141,8 @@ public class DashChunkSource implements ChunkSource {
*/ */
public DashChunkSource(MediaPresentationDescription manifest, int adaptationSetIndex, public DashChunkSource(MediaPresentationDescription manifest, int adaptationSetIndex,
int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator) { int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator) {
this(null, manifest, adaptationSetIndex, representationIndices, dataSource, formatEvaluator, 0); this(null, manifest, adaptationSetIndex, representationIndices, dataSource, formatEvaluator, 0,
0);
} }
/** /**
@ -162,18 +164,21 @@ public class DashChunkSource implements ChunkSource {
* manifest). Choosing a small value will minimize latency introduced by the player, however * manifest). Choosing a small value will minimize latency introduced by the player, however
* note that the value sets an upper bound on the length of media that the player can buffer. * note that the value sets an upper bound on the length of media that the player can buffer.
* Hence a small value may increase the probability of rebuffering and playback failures. * Hence a small value may increase the probability of rebuffering and playback failures.
* @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between
* server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified
* as the server's unix time minus the local elapsed time. It unknown, set to 0.
*/ */
public DashChunkSource(ManifestFetcher<MediaPresentationDescription> manifestFetcher, public DashChunkSource(ManifestFetcher<MediaPresentationDescription> manifestFetcher,
int adaptationSetIndex, int[] representationIndices, DataSource dataSource, int adaptationSetIndex, int[] representationIndices, DataSource dataSource,
FormatEvaluator formatEvaluator, long liveEdgeLatencyMs) { FormatEvaluator formatEvaluator, long liveEdgeLatencyMs, long elapsedRealtimeOffsetMs) {
this(manifestFetcher, manifestFetcher.getManifest(), adaptationSetIndex, representationIndices, this(manifestFetcher, manifestFetcher.getManifest(), adaptationSetIndex, representationIndices,
dataSource, formatEvaluator, liveEdgeLatencyMs * 1000); dataSource, formatEvaluator, liveEdgeLatencyMs * 1000, elapsedRealtimeOffsetMs * 1000);
} }
private DashChunkSource(ManifestFetcher<MediaPresentationDescription> manifestFetcher, private DashChunkSource(ManifestFetcher<MediaPresentationDescription> manifestFetcher,
MediaPresentationDescription initialManifest, int adaptationSetIndex, MediaPresentationDescription initialManifest, int adaptationSetIndex,
int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator, int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator,
long liveEdgeLatencyUs) { long liveEdgeLatencyUs, long elapsedRealtimeOffsetUs) {
this.manifestFetcher = manifestFetcher; this.manifestFetcher = manifestFetcher;
this.currentManifest = initialManifest; this.currentManifest = initialManifest;
this.adaptationSetIndex = adaptationSetIndex; this.adaptationSetIndex = adaptationSetIndex;
@ -181,6 +186,7 @@ public class DashChunkSource implements ChunkSource {
this.dataSource = dataSource; this.dataSource = dataSource;
this.evaluator = formatEvaluator; this.evaluator = formatEvaluator;
this.liveEdgeLatencyUs = liveEdgeLatencyUs; this.liveEdgeLatencyUs = liveEdgeLatencyUs;
this.elapsedRealtimeOffsetUs = elapsedRealtimeOffsetUs;
this.evaluation = new Evaluation(); this.evaluation = new Evaluation();
this.headerBuilder = new StringBuilder(); this.headerBuilder = new StringBuilder();
@ -326,8 +332,12 @@ public class DashChunkSource implements ChunkSource {
return; return;
} }
// TODO: Use UtcTimingElement where possible. long nowUs;
long nowUs = System.currentTimeMillis() * 1000; if (elapsedRealtimeOffsetUs != 0) {
nowUs = (SystemClock.elapsedRealtime() * 1000) + elapsedRealtimeOffsetUs;
} else {
nowUs = System.currentTimeMillis() * 1000;
}
int firstAvailableSegmentNum = segmentIndex.getFirstSegmentNum(); int firstAvailableSegmentNum = segmentIndex.getFirstSegmentNum();
int lastAvailableSegmentNum = segmentIndex.getLastSegmentNum(); int lastAvailableSegmentNum = segmentIndex.getLastSegmentNum();

View File

@ -28,4 +28,9 @@ public class UtcTimingElement {
this.value = value; this.value = value;
} }
@Override
public String toString() {
return schemeIdUri + ", " + value;
}
} }

View File

@ -0,0 +1,186 @@
/*
* 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.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.NetworkLoadable;
import com.google.android.exoplayer.util.Util;
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.concurrent.CancellationException;
/**
* Resolves a {@link UtcTimingElement}.
*/
public 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 static final int TYPE_XS = 0;
private static final int TYPE_ISO = 1;
private final String userAgent;
private final UtcTimingElement timingElement;
private final long timingElementElapsedRealtime;
private final UtcTimingCallback callback;
private Loader singleUseLoader;
private HttpTimestampLoadable singleUseLoadable;
/**
* Resolves a {@link UtcTimingElement}.
*
* @param userAgent A user agent to use should network requests 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(String userAgent, UtcTimingElement timingElement,
long timingElementElapsedRealtime, UtcTimingCallback callback) {
UtcTimingElementResolver resolver = new UtcTimingElementResolver(userAgent, timingElement,
timingElementElapsedRealtime, callback);
resolver.resolve();
}
private UtcTimingElementResolver(String userAgent, UtcTimingElement timingElement,
long timingElementElapsedRealtime, UtcTimingCallback callback) {
this.userAgent = userAgent;
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(TYPE_ISO);
} else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012")
|| Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014")) {
resolveHttp(TYPE_XS);
} 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(int type) {
singleUseLoader = new Loader("utctiming");
singleUseLoadable = new HttpTimestampLoadable(timingElement.value, userAgent, type);
singleUseLoader.startLoading(singleUseLoadable, this);
}
@Override
public void onLoadCanceled(Loadable loadable) {
onLoadError(loadable, new IOException("Load cancelled", new CancellationException()));
}
@Override
public void onLoadCompleted(Loadable loadable) {
releaseLoader();
long elapsedRealtimeOffset = singleUseLoadable.getResult() - SystemClock.elapsedRealtime();
callback.onTimestampResolved(timingElement, elapsedRealtimeOffset);
}
@Override
public void onLoadError(Loadable loadable, IOException exception) {
releaseLoader();
callback.onTimestampError(timingElement, exception);
}
private void releaseLoader() {
singleUseLoader.release();
}
private static class HttpTimestampLoadable extends NetworkLoadable<Long> {
private final int type;
public HttpTimestampLoadable(String url, String userAgent, int type) {
super(url, userAgent);
this.type = type;
}
@Override
protected Long parse(String connectionUrl, InputStream inputStream, String inputEncoding)
throws ParserException, IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String firstLine = reader.readLine();
try {
switch (type) {
case TYPE_XS:
return Util.parseXsDateTime(firstLine);
case TYPE_ISO:
// 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);
return format.parse(firstLine).getTime();
default:
// Never happens.
throw new RuntimeException();
}
} catch (ParseException e) {
throw new ParserException(e);
}
}
}
}

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.util; package com.google.android.exoplayer.util;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.upstream.Loader.Loadable;
@ -25,8 +26,6 @@ import android.util.Pair;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
/** /**
@ -84,7 +83,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
private int enabledCount; private int enabledCount;
private Loader loader; private Loader loader;
private ManifestLoadable currentLoadable; private ManifestLoadable<T> currentLoadable;
private int loadExceptionCount; private int loadExceptionCount;
private long loadExceptionTimestamp; private long loadExceptionTimestamp;
@ -204,7 +203,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
loader = new Loader("manifestLoader"); loader = new Loader("manifestLoader");
} }
if (!loader.isLoading()) { if (!loader.isLoading()) {
currentLoadable = new ManifestLoadable(); currentLoadable = new ManifestLoadable<T>(manifestUrl, userAgent, contentId, parser);
loader.startLoading(currentLoadable, this); loader.startLoading(currentLoadable, this);
notifyManifestRefreshStarted(); notifyManifestRefreshStarted();
} }
@ -217,7 +216,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
return; return;
} }
manifest = currentLoadable.result; manifest = currentLoadable.getResult();
manifestLoadTimestamp = SystemClock.elapsedRealtime(); manifestLoadTimestamp = SystemClock.elapsedRealtime();
loadExceptionCount = 0; loadExceptionCount = 0;
loadException = null; loadException = null;
@ -244,6 +243,11 @@ public class ManifestFetcher<T> implements Loader.Callback {
notifyManifestError(loadException); notifyManifestError(loadException);
} }
/* package */ void onSingleFetchCompleted(T result) {
manifest = result;
manifestLoadTimestamp = SystemClock.elapsedRealtime();
}
private long getRetryDelayMillis(long errorCount) { private long getRetryDelayMillis(long errorCount) {
return Math.min((errorCount - 1) * 1000, 5000); return Math.min((errorCount - 1) * 1000, 5000);
} }
@ -286,13 +290,13 @@ public class ManifestFetcher<T> implements Loader.Callback {
private final Looper callbackLooper; private final Looper callbackLooper;
private final ManifestCallback<T> wrappedCallback; private final ManifestCallback<T> wrappedCallback;
private final Loader singleUseLoader; private final Loader singleUseLoader;
private final ManifestLoadable singleUseLoadable; private final ManifestLoadable<T> singleUseLoadable;
public SingleFetchHelper(Looper callbackLooper, ManifestCallback<T> wrappedCallback) { public SingleFetchHelper(Looper callbackLooper, ManifestCallback<T> wrappedCallback) {
this.callbackLooper = callbackLooper; this.callbackLooper = callbackLooper;
this.wrappedCallback = wrappedCallback; this.wrappedCallback = wrappedCallback;
singleUseLoader = new Loader("manifestLoader:single"); singleUseLoader = new Loader("manifestLoader:single");
singleUseLoadable = new ManifestLoadable(); singleUseLoadable = new ManifestLoadable<T>(manifestUrl, userAgent, contentId, parser);
} }
public void startLoading() { public void startLoading() {
@ -302,9 +306,9 @@ public class ManifestFetcher<T> implements Loader.Callback {
@Override @Override
public void onLoadCompleted(Loadable loadable) { public void onLoadCompleted(Loadable loadable) {
try { try {
manifest = singleUseLoadable.result; T result = singleUseLoadable.getResult();
manifestLoadTimestamp = SystemClock.elapsedRealtime(); onSingleFetchCompleted(result);
wrappedCallback.onManifest(contentId, singleUseLoadable.result); wrappedCallback.onManifest(contentId, result);
} finally { } finally {
releaseLoader(); releaseLoader();
} }
@ -336,50 +340,22 @@ public class ManifestFetcher<T> implements Loader.Callback {
} }
private class ManifestLoadable implements Loadable { private static class ManifestLoadable<T> extends NetworkLoadable<T> {
private static final int TIMEOUT_MILLIS = 10000; private final String contentId;
private final ManifestParser<T> parser;
/* package */ volatile T result; public ManifestLoadable(String url, String userAgent, String contentId,
private volatile boolean isCanceled; ManifestParser<T> parser) {
super(url, userAgent);
@Override this.contentId = contentId;
public void cancelLoad() { this.parser = parser;
// We don't actually cancel anything, but we need to record the cancellation so that
// isLoadCanceled can return the correct value.
isCanceled = true;
} }
@Override @Override
public boolean isLoadCanceled() { protected T parse(String connectionUrl, InputStream inputStream, String inputEncoding)
return isCanceled; throws ParserException, IOException {
} return parser.parse(inputStream, inputEncoding, contentId, Util.parseBaseUri(connectionUrl));
@Override
public void load() throws IOException, InterruptedException {
String inputEncoding;
InputStream inputStream = null;
try {
URLConnection connection = configureConnection(new URL(manifestUrl));
inputStream = connection.getInputStream();
inputEncoding = connection.getContentEncoding();
result = parser.parse(inputStream, inputEncoding, contentId,
Util.parseBaseUri(connection.getURL().toString()));
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
private URLConnection configureConnection(URL url) throws IOException {
URLConnection connection = url.openConnection();
connection.setConnectTimeout(TIMEOUT_MILLIS);
connection.setReadTimeout(TIMEOUT_MILLIS);
connection.setDoOutput(false);
connection.setRequestProperty("User-Agent", userAgent);
connection.connect();
return connection;
} }
} }

View File

@ -0,0 +1,119 @@
/*
* 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.ParserException;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
/**
* A {@link Loadable} for loading an object over the network.
*
* @param <T> The type of the object being loaded.
*/
public abstract class NetworkLoadable<T> implements Loadable {
public static final int DEFAULT_TIMEOUT_MILLIS = 10000;
private final String url;
private final String userAgent;
private final int timeoutMillis;
private volatile T result;
private volatile boolean isCanceled;
/**
* @param url The url from which the object should be loaded.
* @param userAgent The user agent to use when requesting the object.
*/
public NetworkLoadable(String url, String userAgent) {
this(url, userAgent, DEFAULT_TIMEOUT_MILLIS);
}
/**
* @param url The url from which the object should be loaded.
* @param userAgent The user agent to use when requesting the object.
* @param timeoutMillis The desired http timeout in milliseconds.
*/
public NetworkLoadable(String url, String userAgent, int timeoutMillis) {
this.url = url;
this.userAgent = userAgent;
this.timeoutMillis = timeoutMillis;
}
/**
* Returns the loaded object, or null if an object has not been loaded.
*/
public final T getResult() {
return result;
}
@Override
public final 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 final boolean isLoadCanceled() {
return isCanceled;
}
@Override
public final void load() throws IOException, InterruptedException {
String inputEncoding;
InputStream inputStream = null;
try {
URLConnection connection = configureConnection(new URL(url));
inputStream = connection.getInputStream();
inputEncoding = connection.getContentEncoding();
result = parse(connection.getURL().toString(), inputStream, inputEncoding);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
/**
* Parses the raw data into an object.
*
* @param connectionUrl The url, after any redirection has taken place.
* @param inputStream An {@link InputStream} from which the raw data can be read.
* @param inputEncoding The encoding of the raw data, if available.
* @return The parsed object.
* @throws ParserException If an error occurs parsing the data.
* @throws IOException If an error occurs reading data from the stream.
*/
protected abstract T parse(String connectionUrl, InputStream inputStream, String inputEncoding)
throws ParserException, IOException;
private URLConnection configureConnection(URL url) throws IOException {
URLConnection connection = url.openConnection();
connection.setConnectTimeout(timeoutMillis);
connection.setReadTimeout(timeoutMillis);
connection.setDoOutput(false);
connection.setRequestProperty("User-Agent", userAgent);
connection.connect();
return connection;
}
}