mirror of
https://github.com/androidx/media.git
synced 2025-05-09 16:40:55 +08:00
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:
parent
4076b08e4b
commit
6d14fc3330
@ -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.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.demo.DemoUtil;
|
||||
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder;
|
||||
import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallback;
|
||||
@ -59,6 +62,7 @@ import android.annotation.TargetApi;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.UnsupportedSchemeException;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.widget.TextView;
|
||||
|
||||
@ -70,7 +74,9 @@ import java.util.List;
|
||||
* A {@link RendererBuilder} for DASH.
|
||||
*/
|
||||
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 VIDEO_BUFFER_SEGMENTS = 200;
|
||||
@ -96,6 +102,9 @@ public class DashRendererBuilder implements RendererBuilder,
|
||||
private RendererBuilderCallback callback;
|
||||
private ManifestFetcher<MediaPresentationDescription> manifestFetcher;
|
||||
|
||||
private MediaPresentationDescription manifest;
|
||||
private long elapsedRealtimeOffset;
|
||||
|
||||
public DashRendererBuilder(String userAgent, String url, String contentId,
|
||||
MediaDrmCallback drmCallback, TextView debugTextView, AudioCapabilities audioCapabilities) {
|
||||
this.userAgent = userAgent;
|
||||
@ -116,13 +125,36 @@ public class DashRendererBuilder implements RendererBuilder,
|
||||
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
|
||||
public void onManifestError(String contentId, IOException e) {
|
||||
callback.onRenderersError(e);
|
||||
}
|
||||
|
||||
@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);
|
||||
Handler mainHandler = player.getMainHandler();
|
||||
LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE));
|
||||
@ -207,7 +239,7 @@ public class DashRendererBuilder implements RendererBuilder,
|
||||
DataSource videoDataSource = new UriDataSource(userAgent, bandwidthMeter);
|
||||
ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex,
|
||||
videoRepresentationIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter),
|
||||
LIVE_EDGE_LATENCY_MS);
|
||||
LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset);
|
||||
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
||||
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
|
||||
DemoPlayer.TYPE_VIDEO);
|
||||
@ -230,7 +262,8 @@ public class DashRendererBuilder implements RendererBuilder,
|
||||
audioTrackNameList.add(format.id + " (" + format.numChannels + "ch, " +
|
||||
format.audioSamplingRate + "Hz)");
|
||||
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);
|
||||
}
|
||||
// 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);
|
||||
textTrackNameList.add(representation.format.id);
|
||||
textChunkSourceList.add(new DashChunkSource(manifestFetcher, i, new int[] {j},
|
||||
textDataSource, textEvaluator, LIVE_EDGE_LATENCY_MS));
|
||||
textDataSource, textEvaluator, LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -86,6 +86,7 @@ public class DashChunkSource implements ChunkSource {
|
||||
private final Evaluation evaluation;
|
||||
private final StringBuilder headerBuilder;
|
||||
private final long liveEdgeLatencyUs;
|
||||
private final long elapsedRealtimeOffsetUs;
|
||||
private final int maxWidth;
|
||||
private final int maxHeight;
|
||||
|
||||
@ -140,7 +141,8 @@ public class DashChunkSource implements ChunkSource {
|
||||
*/
|
||||
public DashChunkSource(MediaPresentationDescription manifest, int adaptationSetIndex,
|
||||
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
|
||||
* 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.
|
||||
* @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,
|
||||
int adaptationSetIndex, int[] representationIndices, DataSource dataSource,
|
||||
FormatEvaluator formatEvaluator, long liveEdgeLatencyMs) {
|
||||
FormatEvaluator formatEvaluator, long liveEdgeLatencyMs, long elapsedRealtimeOffsetMs) {
|
||||
this(manifestFetcher, manifestFetcher.getManifest(), adaptationSetIndex, representationIndices,
|
||||
dataSource, formatEvaluator, liveEdgeLatencyMs * 1000);
|
||||
dataSource, formatEvaluator, liveEdgeLatencyMs * 1000, elapsedRealtimeOffsetMs * 1000);
|
||||
}
|
||||
|
||||
private DashChunkSource(ManifestFetcher<MediaPresentationDescription> manifestFetcher,
|
||||
MediaPresentationDescription initialManifest, int adaptationSetIndex,
|
||||
int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator,
|
||||
long liveEdgeLatencyUs) {
|
||||
long liveEdgeLatencyUs, long elapsedRealtimeOffsetUs) {
|
||||
this.manifestFetcher = manifestFetcher;
|
||||
this.currentManifest = initialManifest;
|
||||
this.adaptationSetIndex = adaptationSetIndex;
|
||||
@ -181,6 +186,7 @@ public class DashChunkSource implements ChunkSource {
|
||||
this.dataSource = dataSource;
|
||||
this.evaluator = formatEvaluator;
|
||||
this.liveEdgeLatencyUs = liveEdgeLatencyUs;
|
||||
this.elapsedRealtimeOffsetUs = elapsedRealtimeOffsetUs;
|
||||
this.evaluation = new Evaluation();
|
||||
this.headerBuilder = new StringBuilder();
|
||||
|
||||
@ -326,8 +332,12 @@ public class DashChunkSource implements ChunkSource {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Use UtcTimingElement where possible.
|
||||
long nowUs = System.currentTimeMillis() * 1000;
|
||||
long nowUs;
|
||||
if (elapsedRealtimeOffsetUs != 0) {
|
||||
nowUs = (SystemClock.elapsedRealtime() * 1000) + elapsedRealtimeOffsetUs;
|
||||
} else {
|
||||
nowUs = System.currentTimeMillis() * 1000;
|
||||
}
|
||||
|
||||
int firstAvailableSegmentNum = segmentIndex.getFirstSegmentNum();
|
||||
int lastAvailableSegmentNum = segmentIndex.getLastSegmentNum();
|
||||
|
@ -28,4 +28,9 @@ public class UtcTimingElement {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return schemeIdUri + ", " + value;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
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.Loadable;
|
||||
|
||||
@ -25,8 +26,6 @@ import android.util.Pair;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
/**
|
||||
@ -84,7 +83,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
|
||||
|
||||
private int enabledCount;
|
||||
private Loader loader;
|
||||
private ManifestLoadable currentLoadable;
|
||||
private ManifestLoadable<T> currentLoadable;
|
||||
|
||||
private int loadExceptionCount;
|
||||
private long loadExceptionTimestamp;
|
||||
@ -204,7 +203,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
|
||||
loader = new Loader("manifestLoader");
|
||||
}
|
||||
if (!loader.isLoading()) {
|
||||
currentLoadable = new ManifestLoadable();
|
||||
currentLoadable = new ManifestLoadable<T>(manifestUrl, userAgent, contentId, parser);
|
||||
loader.startLoading(currentLoadable, this);
|
||||
notifyManifestRefreshStarted();
|
||||
}
|
||||
@ -217,7 +216,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
|
||||
return;
|
||||
}
|
||||
|
||||
manifest = currentLoadable.result;
|
||||
manifest = currentLoadable.getResult();
|
||||
manifestLoadTimestamp = SystemClock.elapsedRealtime();
|
||||
loadExceptionCount = 0;
|
||||
loadException = null;
|
||||
@ -244,6 +243,11 @@ public class ManifestFetcher<T> implements Loader.Callback {
|
||||
notifyManifestError(loadException);
|
||||
}
|
||||
|
||||
/* package */ void onSingleFetchCompleted(T result) {
|
||||
manifest = result;
|
||||
manifestLoadTimestamp = SystemClock.elapsedRealtime();
|
||||
}
|
||||
|
||||
private long getRetryDelayMillis(long errorCount) {
|
||||
return Math.min((errorCount - 1) * 1000, 5000);
|
||||
}
|
||||
@ -286,13 +290,13 @@ public class ManifestFetcher<T> implements Loader.Callback {
|
||||
private final Looper callbackLooper;
|
||||
private final ManifestCallback<T> wrappedCallback;
|
||||
private final Loader singleUseLoader;
|
||||
private final ManifestLoadable singleUseLoadable;
|
||||
private final ManifestLoadable<T> singleUseLoadable;
|
||||
|
||||
public SingleFetchHelper(Looper callbackLooper, ManifestCallback<T> wrappedCallback) {
|
||||
this.callbackLooper = callbackLooper;
|
||||
this.wrappedCallback = wrappedCallback;
|
||||
singleUseLoader = new Loader("manifestLoader:single");
|
||||
singleUseLoadable = new ManifestLoadable();
|
||||
singleUseLoadable = new ManifestLoadable<T>(manifestUrl, userAgent, contentId, parser);
|
||||
}
|
||||
|
||||
public void startLoading() {
|
||||
@ -302,9 +306,9 @@ public class ManifestFetcher<T> implements Loader.Callback {
|
||||
@Override
|
||||
public void onLoadCompleted(Loadable loadable) {
|
||||
try {
|
||||
manifest = singleUseLoadable.result;
|
||||
manifestLoadTimestamp = SystemClock.elapsedRealtime();
|
||||
wrappedCallback.onManifest(contentId, singleUseLoadable.result);
|
||||
T result = singleUseLoadable.getResult();
|
||||
onSingleFetchCompleted(result);
|
||||
wrappedCallback.onManifest(contentId, result);
|
||||
} finally {
|
||||
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;
|
||||
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;
|
||||
public ManifestLoadable(String url, String userAgent, String contentId,
|
||||
ManifestParser<T> parser) {
|
||||
super(url, userAgent);
|
||||
this.contentId = contentId;
|
||||
this.parser = parser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoadCanceled() {
|
||||
return isCanceled;
|
||||
}
|
||||
|
||||
@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;
|
||||
protected T parse(String connectionUrl, InputStream inputStream, String inputEncoding)
|
||||
throws ParserException, IOException {
|
||||
return parser.parse(inputStream, inputEncoding, contentId, Util.parseBaseUri(connectionUrl));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user