From 006986cc58bae6758cd75ea7bc21a0da4eae9646 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 25 Feb 2015 13:39:15 +0000 Subject: [PATCH] Make NetworkLoadable use an injected HttpDataSource. Note: I'm fairly confident that NetworkLoadable.Parser implementations can live without the inputEncoding being specified. But not completely 100%... Issue: #311 Issue: #56 --- .../demo/player/DashRendererBuilder.java | 9 +- .../demo/player/HlsRendererBuilder.java | 3 +- .../SmoothStreamingRendererBuilder.java | 5 +- .../MediaPresentationDescriptionParser.java | 8 +- .../dash/mpd/UtcTimingElementResolver.java | 28 +- .../android/exoplayer/hls/HlsChunkSource.java | 2 +- .../exoplayer/hls/HlsPlaylistParser.java | 7 +- .../SmoothStreamingManifestParser.java | 8 +- .../upstream/DataSourceInputStream.java | 13 + .../upstream/DefaultHttpDataSource.java | 358 ++++++++++++++++++ .../exoplayer/upstream/HttpDataSource.java | 329 +--------------- .../{util => upstream}/NetworkLoadable.java | 59 +-- .../exoplayer/upstream/UriDataSource.java | 4 +- .../exoplayer/util/ManifestFetcher.java | 35 +- 14 files changed, 464 insertions(+), 404 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/upstream/DefaultHttpDataSource.java rename library/src/main/java/com/google/android/exoplayer/{util => upstream}/NetworkLoadable.java (55%) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java index ab0c316697..54023fb8bb 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java @@ -52,6 +52,8 @@ import com.google.android.exoplayer.text.webvtt.WebvttParser; import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer.upstream.DefaultHttpDataSource; +import com.google.android.exoplayer.upstream.HttpDataSource; import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; @@ -100,6 +102,7 @@ public class DashRendererBuilder implements RendererBuilder, private DemoPlayer player; private RendererBuilderCallback callback; private ManifestFetcher manifestFetcher; + private HttpDataSource manifestDataSource; private MediaPresentationDescription manifest; private long elapsedRealtimeOffset; @@ -118,7 +121,9 @@ public class DashRendererBuilder implements RendererBuilder, this.player = player; this.callback = callback; MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); - manifestFetcher = new ManifestFetcher(url, userAgent, parser); + manifestDataSource = new DefaultHttpDataSource(userAgent, null); + manifestFetcher = new ManifestFetcher(url, manifestDataSource, + parser); manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); } @@ -126,7 +131,7 @@ public class DashRendererBuilder implements RendererBuilder, public void onSingleManifest(MediaPresentationDescription manifest) { this.manifest = manifest; if (manifest.dynamic && manifest.utcTiming != null) { - UtcTimingElementResolver.resolveTimingElement(userAgent, manifest.utcTiming, + UtcTimingElementResolver.resolveTimingElement(manifestDataSource, manifest.utcTiming, manifestFetcher.getManifestLoadTimestamp(), this); } else { buildRenderers(); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java index d0c8465bbe..03afe7190b 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer.metadata.MetadataTrackRenderer; import com.google.android.exoplayer.text.eia608.Eia608TrackRenderer; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer.upstream.DefaultHttpDataSource; import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; @@ -60,7 +61,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback playlistFetcher = - new ManifestFetcher(url, userAgent, parser); + new ManifestFetcher(url, new DefaultHttpDataSource(userAgent, null), parser); playlistFetcher.singleLoad(player.getMainHandler().getLooper(), this); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/SmoothStreamingRendererBuilder.java index 1d22f6cc07..1e8ce37a54 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/SmoothStreamingRendererBuilder.java @@ -42,6 +42,7 @@ import com.google.android.exoplayer.text.ttml.TtmlParser; import com.google.android.exoplayer.upstream.BufferPool; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer.upstream.DefaultHttpDataSource; import com.google.android.exoplayer.upstream.UriDataSource; import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.Util; @@ -90,8 +91,8 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, this.player = player; this.callback = callback; SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); - manifestFetcher = new ManifestFetcher(url + "/Manifest", userAgent, - parser); + manifestFetcher = new ManifestFetcher(url + "/Manifest", + new DefaultHttpDataSource(userAgent, null), parser); manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java index 2b969a7165..813ce0c4e0 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java @@ -21,9 +21,9 @@ import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentList; import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTemplate; import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTimelineElement; import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase; +import com.google.android.exoplayer.upstream.NetworkLoadable; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.MimeTypes; -import com.google.android.exoplayer.util.NetworkLoadable; import com.google.android.exoplayer.util.Util; import android.net.Uri; @@ -73,11 +73,11 @@ public class MediaPresentationDescriptionParser extends DefaultHandler // MPD parsing. @Override - public MediaPresentationDescription parse(String connectionUrl, InputStream inputStream, - String inputEncoding) throws IOException, ParserException { + public MediaPresentationDescription parse(String connectionUrl, InputStream inputStream) + throws IOException, ParserException { try { XmlPullParser xpp = xmlParserFactory.newPullParser(); - xpp.setInput(inputStream, inputEncoding); + xpp.setInput(inputStream, null); int eventType = xpp.next(); if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) { throw new ParserException( 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 index 1220bdd89d..bfb8d042b2 100644 --- 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 @@ -16,10 +16,11 @@ package com.google.android.exoplayer.dash.mpd; import com.google.android.exoplayer.ParserException; +import com.google.android.exoplayer.upstream.HttpDataSource; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader.Loadable; +import com.google.android.exoplayer.upstream.NetworkLoadable; 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; @@ -62,7 +63,7 @@ public class UtcTimingElementResolver implements Loader.Callback { void onTimestampError(UtcTimingElement utcTiming, IOException e); } - private final String userAgent; + private final HttpDataSource httpDataSource; private final UtcTimingElement timingElement; private final long timingElementElapsedRealtime; private final UtcTimingCallback callback; @@ -73,22 +74,23 @@ public class UtcTimingElementResolver implements Loader.Callback { /** * Resolves a {@link UtcTimingElement}. * - * @param userAgent A user agent to use should network requests be necessary. + * @param httpDataSource A source 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, + public static void resolveTimingElement(HttpDataSource httpDataSource, + UtcTimingElement timingElement, long timingElementElapsedRealtime, + UtcTimingCallback callback) { + UtcTimingElementResolver resolver = new UtcTimingElementResolver(httpDataSource, timingElement, timingElementElapsedRealtime, callback); resolver.resolve(); } - private UtcTimingElementResolver(String userAgent, UtcTimingElement timingElement, + private UtcTimingElementResolver(HttpDataSource httpDataSource, UtcTimingElement timingElement, long timingElementElapsedRealtime, UtcTimingCallback callback) { - this.userAgent = userAgent; + this.httpDataSource = httpDataSource; this.timingElement = Assertions.checkNotNull(timingElement); this.timingElementElapsedRealtime = timingElementElapsedRealtime; this.callback = Assertions.checkNotNull(callback); @@ -121,7 +123,7 @@ public class UtcTimingElementResolver implements Loader.Callback { private void resolveHttp(NetworkLoadable.Parser parser) { singleUseLoader = new Loader("utctiming"); - singleUseLoadable = new NetworkLoadable(timingElement.value, userAgent, parser); + singleUseLoadable = new NetworkLoadable(timingElement.value, httpDataSource, parser); singleUseLoader.startLoading(singleUseLoadable, this); } @@ -150,8 +152,8 @@ public class UtcTimingElementResolver implements Loader.Callback { private static class XsDateTimeParser implements NetworkLoadable.Parser { @Override - public Long parse(String connectionUrl, InputStream inputStream, String inputEncoding) - throws ParserException, IOException { + public Long parse(String connectionUrl, InputStream inputStream) throws ParserException, + IOException { String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); try { return Util.parseXsDateTime(firstLine); @@ -165,8 +167,8 @@ public class UtcTimingElementResolver implements Loader.Callback { private static class Iso8601Parser implements NetworkLoadable.Parser { @Override - public Long parse(String connectionUrl, InputStream inputStream, String inputEncoding) - throws ParserException, IOException { + public Long parse(String connectionUrl, InputStream inputStream) throws ParserException, + IOException { String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); try { // TODO: It may be necessary to handle timestamp offsets from UTC. diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java index f2469f294f..1a4025ef89 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java @@ -558,7 +558,7 @@ public class HlsChunkSource { @Override protected void consume(byte[] data, int limit) throws IOException { HlsPlaylist playlist = playlistParser.parse(playlistUrl, - new ByteArrayInputStream(data, 0, limit), null); + new ByteArrayInputStream(data, 0, limit)); Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MEDIA); HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; setMediaPlaylist(variantIndex, mediaPlaylist); diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java index d9d1e6c415..8db2094c9f 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java @@ -18,7 +18,7 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.C; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.hls.HlsMediaPlaylist.Segment; -import com.google.android.exoplayer.util.NetworkLoadable; +import com.google.android.exoplayer.upstream.NetworkLoadable; import com.google.android.exoplayer.util.Util; import android.net.Uri; @@ -84,11 +84,10 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser extraLines = new LinkedList(); String line; try { diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java index 47a90da919..5cfbc829e3 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java @@ -19,10 +19,10 @@ import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement; +import com.google.android.exoplayer.upstream.NetworkLoadable; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.MimeTypes; -import com.google.android.exoplayer.util.NetworkLoadable; import com.google.android.exoplayer.util.Util; import android.net.Uri; @@ -60,11 +60,11 @@ public class SmoothStreamingManifestParser implements } @Override - public SmoothStreamingManifest parse(String connectionUrl, InputStream inputStream, - String inputEncoding) throws IOException, ParserException { + public SmoothStreamingManifest parse(String connectionUrl, InputStream inputStream) + throws IOException, ParserException { try { XmlPullParser xmlParser = xmlParserFactory.newPullParser(); - xmlParser.setInput(inputStream, inputEncoding); + xmlParser.setInput(inputStream, null); SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null, Util.parseBaseUri(connectionUrl)); return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser); diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceInputStream.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceInputStream.java index e0a01f3a9a..e81e1990de 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceInputStream.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceInputStream.java @@ -43,6 +43,19 @@ public class DataSourceInputStream extends InputStream { singleByteArray = new byte[1]; } + /** + * Optional call to open the underlying {@link DataSource}. + *

+ * Calling this method does nothing if the {@link DataSource} is already open. Calling this + * method is optional, since the read and skip methods will automatically open the underlying + * {@link DataSource} if it's not open already. + * + * @throws IOException If an error occurs opening the {@link DataSource}. + */ + public void open() throws IOException { + checkOpened(); + } + @Override public int read() throws IOException { read(singleByteArray); diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultHttpDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultHttpDataSource.java new file mode 100644 index 0000000000..6fbab8982e --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultHttpDataSource.java @@ -0,0 +1,358 @@ +/* + * 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.upstream; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.Predicate; + +import android.text.TextUtils; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A {@link HttpDataSource} that uses Android's {@link HttpURLConnection}. + */ +public class DefaultHttpDataSource implements HttpDataSource { + + public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000; + public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000; + + private static final String TAG = "HttpDataSource"; + private static final Pattern CONTENT_RANGE_HEADER = + Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$"); + + private final int connectTimeoutMillis; + private final int readTimeoutMillis; + private final String userAgent; + private final Predicate contentTypePredicate; + private final HashMap requestProperties; + private final TransferListener listener; + + private DataSpec dataSpec; + private HttpURLConnection connection; + private InputStream inputStream; + private boolean opened; + + private long dataLength; + private long bytesRead; + + /** + * @param userAgent The User-Agent string that should be used. + * @param contentTypePredicate An optional {@link Predicate}. If a content type is + * rejected by the predicate then a {@link HttpDataSource.InvalidContentTypeException} is + * thrown from {@link #open(DataSpec)}. + */ + public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate) { + this(userAgent, contentTypePredicate, null); + } + + /** + * @param userAgent The User-Agent string that should be used. + * @param contentTypePredicate An optional {@link Predicate}. If a content type is + * rejected by the predicate then a {@link HttpDataSource.InvalidContentTypeException} is + * thrown from {@link #open(DataSpec)}. + * @param listener An optional listener. + */ + public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, + TransferListener listener) { + this(userAgent, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS, + DEFAULT_READ_TIMEOUT_MILLIS); + } + + /** + * @param userAgent The User-Agent string that should be used. + * @param contentTypePredicate An optional {@link Predicate}. If a content type is + * rejected by the predicate then a {@link HttpDataSource.InvalidContentTypeException} is + * thrown from {@link #open(DataSpec)}. + * @param listener An optional listener. + * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is + * interpreted as an infinite timeout. + * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted + * as an infinite timeout. + */ + public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, + TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) { + this.userAgent = Assertions.checkNotEmpty(userAgent); + this.contentTypePredicate = contentTypePredicate; + this.listener = listener; + this.requestProperties = new HashMap(); + this.connectTimeoutMillis = connectTimeoutMillis; + this.readTimeoutMillis = readTimeoutMillis; + } + + @Override + public String getUrl() { + return connection == null ? null : connection.getURL().toString(); + } + + @Override + public void setRequestProperty(String name, String value) { + Assertions.checkNotNull(name); + Assertions.checkNotNull(value); + synchronized (requestProperties) { + requestProperties.put(name, value); + } + } + + @Override + public void clearRequestProperty(String name) { + Assertions.checkNotNull(name); + synchronized (requestProperties) { + requestProperties.remove(name); + } + } + + @Override + public void clearAllRequestProperties() { + synchronized (requestProperties) { + requestProperties.clear(); + } + } + + /* + * TODO: If the server uses gzip compression when serving the response, this may end up returning + * the size of the compressed response, where-as it should be returning the decompressed size or + * -1. See: developer.android.com/reference/java/net/HttpURLConnection.html + * + * To fix this we should: + * + * 1. Explicitly require no compression for media requests (since media should be compressed + * already) by setting the Accept-Encoding header to "identity" + * 2. In other cases, for example when requesting manifests, we don't want to disable compression. + * For these cases we should ensure that we return -1 here (and avoid performing any sanity + * checks on the content length). + */ + @Override + public long open(DataSpec dataSpec) throws HttpDataSourceException { + this.dataSpec = dataSpec; + this.bytesRead = 0; + try { + connection = makeConnection(dataSpec); + } catch (IOException e) { + throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, + dataSpec); + } + + // Check for a valid response code. + int responseCode; + try { + responseCode = connection.getResponseCode(); + } catch (IOException e) { + throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, + dataSpec); + } + if (responseCode < 200 || responseCode > 299) { + Map> headers = connection.getHeaderFields(); + closeConnection(); + throw new InvalidResponseCodeException(responseCode, headers, dataSpec); + } + + // Check for a valid content type. + String contentType = connection.getContentType(); + if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) { + closeConnection(); + throw new InvalidContentTypeException(contentType, dataSpec); + } + + long contentLength = getContentLength(connection); + dataLength = dataSpec.length == C.LENGTH_UNBOUNDED ? contentLength : dataSpec.length; + + if (dataSpec.length != C.LENGTH_UNBOUNDED && contentLength != C.LENGTH_UNBOUNDED + && contentLength != dataSpec.length) { + // The DataSpec specified a length and we resolved a length from the response headers, but + // the two lengths do not match. + closeConnection(); + throw new HttpDataSourceException( + new UnexpectedLengthException(dataSpec.length, contentLength), dataSpec); + } + + try { + inputStream = connection.getInputStream(); + } catch (IOException e) { + closeConnection(); + throw new HttpDataSourceException(e, dataSpec); + } + + opened = true; + if (listener != null) { + listener.onTransferStart(); + } + + return dataLength; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException { + int read = 0; + try { + read = inputStream.read(buffer, offset, readLength); + } catch (IOException e) { + throw new HttpDataSourceException(e, dataSpec); + } + + if (read > 0) { + bytesRead += read; + if (listener != null) { + listener.onBytesTransferred(read); + } + } else if (dataLength != C.LENGTH_UNBOUNDED && dataLength != bytesRead) { + // Check for cases where the server closed the connection having not sent the correct amount + // of data. We can only do this if we know the length of the data we were expecting. + throw new HttpDataSourceException(new UnexpectedLengthException(dataLength, bytesRead), + dataSpec); + } + + return read; + } + + @Override + public void close() throws HttpDataSourceException { + try { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + throw new HttpDataSourceException(e, dataSpec); + } + inputStream = null; + } + } finally { + if (opened) { + opened = false; + if (listener != null) { + listener.onTransferEnd(); + } + closeConnection(); + } + } + } + + private void closeConnection() { + if (connection != null) { + connection.disconnect(); + connection = null; + } + } + + /** + * Returns the current connection, or null if the source is not currently opened. + * + * @return The current open connection, or null. + */ + protected final HttpURLConnection getConnection() { + return connection; + } + + /** + * Returns the number of bytes that have been read since the most recent call to + * {@link #open(DataSpec)}. + * + * @return The number of bytes read. + */ + protected final long bytesRead() { + return bytesRead; + } + + /** + * Returns the number of bytes that are still to be read for the current {@link DataSpec}. + *

+ * If the total length of the data being read is known, then this length minus {@code bytesRead()} + * is returned. If the total length is unknown, {@link C#LENGTH_UNBOUNDED} is returned. + * + * @return The remaining length, or {@link C#LENGTH_UNBOUNDED}. + */ + protected final long bytesRemaining() { + return dataLength == C.LENGTH_UNBOUNDED ? dataLength : dataLength - bytesRead; + } + + private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { + URL url = new URL(dataSpec.uri.toString()); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(connectTimeoutMillis); + connection.setReadTimeout(readTimeoutMillis); + connection.setDoOutput(false); + synchronized (requestProperties) { + for (Map.Entry property : requestProperties.entrySet()) { + connection.setRequestProperty(property.getKey(), property.getValue()); + } + } + setRangeHeader(connection, dataSpec); + connection.setRequestProperty("User-Agent", userAgent); + connection.connect(); + return connection; + } + + private void setRangeHeader(HttpURLConnection connection, DataSpec dataSpec) { + if (dataSpec.position == 0 && dataSpec.length == C.LENGTH_UNBOUNDED) { + // Not required. + return; + } + String rangeRequest = "bytes=" + dataSpec.position + "-"; + if (dataSpec.length != C.LENGTH_UNBOUNDED) { + rangeRequest += (dataSpec.position + dataSpec.length - 1); + } + connection.setRequestProperty("Range", rangeRequest); + } + + private long getContentLength(HttpURLConnection connection) { + long contentLength = C.LENGTH_UNBOUNDED; + String contentLengthHeader = connection.getHeaderField("Content-Length"); + if (!TextUtils.isEmpty(contentLengthHeader)) { + try { + contentLength = Long.parseLong(contentLengthHeader); + } catch (NumberFormatException e) { + Log.e(TAG, "Unexpected Content-Length [" + contentLengthHeader + "]"); + } + } + String contentRangeHeader = connection.getHeaderField("Content-Range"); + if (!TextUtils.isEmpty(contentRangeHeader)) { + Matcher matcher = CONTENT_RANGE_HEADER.matcher(contentRangeHeader); + if (matcher.find()) { + try { + long contentLengthFromRange = + Long.parseLong(matcher.group(2)) - Long.parseLong(matcher.group(1)) + 1; + if (contentLength < 0) { + // Some proxy servers strip the Content-Length header. Fall back to the length + // calculated here in this case. + contentLength = contentLengthFromRange; + } else if (contentLength != contentLengthFromRange) { + // If there is a discrepancy between the Content-Length and Content-Range headers, + // assume the one with the larger value is correct. We have seen cases where carrier + // change one of them to reduce the size of a request, but it is unlikely anybody would + // increase it. + Log.w(TAG, "Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader + + "]"); + contentLength = Math.max(contentLength, contentLengthFromRange); + } + } catch (NumberFormatException e) { + Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]"); + } + } + } + return contentLength; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java index fd2f345e4c..c3780896fd 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java @@ -15,28 +15,20 @@ */ package com.google.android.exoplayer.upstream; -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Predicate; import com.google.android.exoplayer.util.Util; import android.text.TextUtils; -import android.util.Log; import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; + /** - * An http {@link DataSource}. + * An HTTP specific extension to {@link DataSource}. */ -public class HttpDataSource implements DataSource { +public interface HttpDataSource extends DataSource { /** * A {@link Predicate} that rejects content types often used for pay-walls. @@ -54,7 +46,7 @@ public class HttpDataSource implements DataSource { }; /** - * Thrown when an error is encountered when trying to read from HTTP data source. + * Thrown when an error is encountered when trying to read from a {@link HttpDataSource}. */ public static class HttpDataSourceException extends IOException { @@ -123,71 +115,14 @@ public class HttpDataSource implements DataSource { } - public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000; - public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000; - - private static final String TAG = "HttpDataSource"; - private static final Pattern CONTENT_RANGE_HEADER = - Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$"); - - private final int connectTimeoutMillis; - private final int readTimeoutMillis; - private final String userAgent; - private final Predicate contentTypePredicate; - private final HashMap requestProperties; - private final TransferListener listener; - - private DataSpec dataSpec; - private HttpURLConnection connection; - private InputStream inputStream; - private boolean opened; - - private long dataLength; - private long bytesRead; - /** - * @param userAgent The User-Agent string that should be used. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is - * rejected by the predicate then a {@link InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. + * When the source is open, returns the url from which data is being read. + *

+ * If redirection occurred, the url after redirection is the one returned. + * + * @return When the source is open, the url from which data is being read. Null otherwise. */ - public HttpDataSource(String userAgent, Predicate contentTypePredicate) { - this(userAgent, contentTypePredicate, null); - } - - /** - * @param userAgent The User-Agent string that should be used. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is - * rejected by the predicate then a {@link InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. - * @param listener An optional listener. - */ - public HttpDataSource(String userAgent, Predicate contentTypePredicate, - TransferListener listener) { - this(userAgent, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS, - DEFAULT_READ_TIMEOUT_MILLIS); - } - - /** - * @param userAgent The User-Agent string that should be used. - * @param contentTypePredicate An optional {@link Predicate}. If a content type is - * rejected by the predicate then a {@link InvalidContentTypeException} is thrown from - * {@link #open(DataSpec)}. - * @param listener An optional listener. - * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is - * interpreted as an infinite timeout. - * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted - * as an infinite timeout. - */ - public HttpDataSource(String userAgent, Predicate contentTypePredicate, - TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) { - this.userAgent = Assertions.checkNotEmpty(userAgent); - this.contentTypePredicate = contentTypePredicate; - this.listener = listener; - this.requestProperties = new HashMap(); - this.connectTimeoutMillis = connectTimeoutMillis; - this.readTimeoutMillis = readTimeoutMillis; - } + String getUrl(); /** * Sets the value of a request header field. The value will be used for subsequent connections @@ -196,13 +131,7 @@ public class HttpDataSource implements DataSource { * @param name The name of the header field. * @param value The value of the field. */ - public void setRequestProperty(String name, String value) { - Assertions.checkNotNull(name); - Assertions.checkNotNull(value); - synchronized (requestProperties) { - requestProperties.put(name, value); - } - } + void setRequestProperty(String name, String value); /** * Clears the value of a request header field. The change will apply to subsequent connections @@ -210,243 +139,11 @@ public class HttpDataSource implements DataSource { * * @param name The name of the header field. */ - public void clearRequestProperty(String name) { - Assertions.checkNotNull(name); - synchronized (requestProperties) { - requestProperties.remove(name); - } - } + void clearRequestProperty(String name); /** * Clears all request header fields that were set by {@link #setRequestProperty(String, String)}. */ - public void clearAllRequestProperties() { - synchronized (requestProperties) { - requestProperties.clear(); - } - } - - /* - * TODO: If the server uses gzip compression when serving the response, this may end up returning - * the size of the compressed response, where-as it should be returning the decompressed size or - * -1. See: developer.android.com/reference/java/net/HttpURLConnection.html - * - * To fix this we should: - * - * 1. Explicitly require no compression for media requests (since media should be compressed - * already) by setting the Accept-Encoding header to "identity" - * 2. In other cases, for example when requesting manifests, we don't want to disable compression. - * For these cases we should ensure that we return -1 here (and avoid performing any sanity - * checks on the content length). - */ - @Override - public long open(DataSpec dataSpec) throws HttpDataSourceException { - this.dataSpec = dataSpec; - this.bytesRead = 0; - try { - connection = makeConnection(dataSpec); - } catch (IOException e) { - throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, - dataSpec); - } - - // Check for a valid response code. - int responseCode; - try { - responseCode = connection.getResponseCode(); - } catch (IOException e) { - throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, - dataSpec); - } - if (responseCode < 200 || responseCode > 299) { - Map> headers = connection.getHeaderFields(); - closeConnection(); - throw new InvalidResponseCodeException(responseCode, headers, dataSpec); - } - - // Check for a valid content type. - String contentType = connection.getContentType(); - if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) { - closeConnection(); - throw new InvalidContentTypeException(contentType, dataSpec); - } - - long contentLength = getContentLength(connection); - dataLength = dataSpec.length == C.LENGTH_UNBOUNDED ? contentLength : dataSpec.length; - - if (dataSpec.length != C.LENGTH_UNBOUNDED && contentLength != C.LENGTH_UNBOUNDED - && contentLength != dataSpec.length) { - // The DataSpec specified a length and we resolved a length from the response headers, but - // the two lengths do not match. - closeConnection(); - throw new HttpDataSourceException( - new UnexpectedLengthException(dataSpec.length, contentLength), dataSpec); - } - - try { - inputStream = connection.getInputStream(); - } catch (IOException e) { - closeConnection(); - throw new HttpDataSourceException(e, dataSpec); - } - - opened = true; - if (listener != null) { - listener.onTransferStart(); - } - - return dataLength; - } - - @Override - public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException { - int read = 0; - try { - read = inputStream.read(buffer, offset, readLength); - } catch (IOException e) { - throw new HttpDataSourceException(e, dataSpec); - } - - if (read > 0) { - bytesRead += read; - if (listener != null) { - listener.onBytesTransferred(read); - } - } else if (dataLength != C.LENGTH_UNBOUNDED && dataLength != bytesRead) { - // Check for cases where the server closed the connection having not sent the correct amount - // of data. We can only do this if we know the length of the data we were expecting. - throw new HttpDataSourceException(new UnexpectedLengthException(dataLength, bytesRead), - dataSpec); - } - - return read; - } - - @Override - public void close() throws HttpDataSourceException { - try { - if (inputStream != null) { - try { - inputStream.close(); - } catch (IOException e) { - throw new HttpDataSourceException(e, dataSpec); - } - inputStream = null; - } - } finally { - if (opened) { - opened = false; - if (listener != null) { - listener.onTransferEnd(); - } - closeConnection(); - } - } - } - - private void closeConnection() { - if (connection != null) { - connection.disconnect(); - connection = null; - } - } - - /** - * Returns the current connection, or null if the source is not currently opened. - * - * @return The current open connection, or null. - */ - protected final HttpURLConnection getConnection() { - return connection; - } - - /** - * Returns the number of bytes that have been read since the most recent call to - * {@link #open(DataSpec)}. - * - * @return The number of bytes read. - */ - protected final long bytesRead() { - return bytesRead; - } - - /** - * Returns the number of bytes that are still to be read for the current {@link DataSpec}. - *

- * If the total length of the data being read is known, then this length minus {@code bytesRead()} - * is returned. If the total length is unknown, {@link C#LENGTH_UNBOUNDED} is returned. - * - * @return The remaining length, or {@link C#LENGTH_UNBOUNDED}. - */ - protected final long bytesRemaining() { - return dataLength == C.LENGTH_UNBOUNDED ? dataLength : dataLength - bytesRead; - } - - private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { - URL url = new URL(dataSpec.uri.toString()); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(connectTimeoutMillis); - connection.setReadTimeout(readTimeoutMillis); - connection.setDoOutput(false); - synchronized (requestProperties) { - for (Map.Entry property : requestProperties.entrySet()) { - connection.setRequestProperty(property.getKey(), property.getValue()); - } - } - setRangeHeader(connection, dataSpec); - connection.setRequestProperty("User-Agent", userAgent); - connection.connect(); - return connection; - } - - private void setRangeHeader(HttpURLConnection connection, DataSpec dataSpec) { - if (dataSpec.position == 0 && dataSpec.length == C.LENGTH_UNBOUNDED) { - // Not required. - return; - } - String rangeRequest = "bytes=" + dataSpec.position + "-"; - if (dataSpec.length != C.LENGTH_UNBOUNDED) { - rangeRequest += (dataSpec.position + dataSpec.length - 1); - } - connection.setRequestProperty("Range", rangeRequest); - } - - private long getContentLength(HttpURLConnection connection) { - long contentLength = C.LENGTH_UNBOUNDED; - String contentLengthHeader = connection.getHeaderField("Content-Length"); - if (!TextUtils.isEmpty(contentLengthHeader)) { - try { - contentLength = Long.parseLong(contentLengthHeader); - } catch (NumberFormatException e) { - Log.e(TAG, "Unexpected Content-Length [" + contentLengthHeader + "]"); - } - } - String contentRangeHeader = connection.getHeaderField("Content-Range"); - if (!TextUtils.isEmpty(contentRangeHeader)) { - Matcher matcher = CONTENT_RANGE_HEADER.matcher(contentRangeHeader); - if (matcher.find()) { - try { - long contentLengthFromRange = - Long.parseLong(matcher.group(2)) - Long.parseLong(matcher.group(1)) + 1; - if (contentLength < 0) { - // Some proxy servers strip the Content-Length header. Fall back to the length - // calculated here in this case. - contentLength = contentLengthFromRange; - } else if (contentLength != contentLengthFromRange) { - // If there is a discrepancy between the Content-Length and Content-Range headers, - // assume the one with the larger value is correct. We have seen cases where carrier - // change one of them to reduce the size of a request, but it is unlikely anybody would - // increase it. - Log.w(TAG, "Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader + - "]"); - contentLength = Math.max(contentLength, contentLengthFromRange); - } - } catch (NumberFormatException e) { - Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]"); - } - } - } - return contentLength; - } + void clearAllRequestProperties(); } diff --git a/library/src/main/java/com/google/android/exoplayer/util/NetworkLoadable.java b/library/src/main/java/com/google/android/exoplayer/upstream/NetworkLoadable.java similarity index 55% rename from library/src/main/java/com/google/android/exoplayer/util/NetworkLoadable.java rename to library/src/main/java/com/google/android/exoplayer/upstream/NetworkLoadable.java index 9e38795e7d..5e8058f6dd 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/NetworkLoadable.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/NetworkLoadable.java @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer.util; +package com.google.android.exoplayer.upstream; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.upstream.Loader.Loadable; +import android.net.Uri; + 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. @@ -40,21 +40,16 @@ public final class NetworkLoadable implements Loadable { * * @param connectionUrl The source of the response, after any redirection. * @param inputStream An {@link InputStream} from which the response data can be read. - * @param inputEncoding The encoding of the 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. */ - T parse(String connectionUrl, InputStream inputStream, String inputEncoding) - throws ParserException, IOException; + T parse(String connectionUrl, InputStream inputStream) throws ParserException, IOException; } - public static final int DEFAULT_TIMEOUT_MILLIS = 10000; - - private final String url; - private final String userAgent; - private final int timeoutMillis; + private final DataSpec dataSpec; + private final HttpDataSource httpDataSource; private final Parser parser; private volatile T result; @@ -62,24 +57,13 @@ public final class NetworkLoadable implements Loadable { /** * @param url The url from which the object should be loaded. - * @param userAgent The user agent to use when requesting the object. + * @param httpDataSource A {@link HttpDataSource} to use when loading the data. * @param parser Parses the object from the network response. */ - public NetworkLoadable(String url, String userAgent, Parser parser) { - this(url, userAgent, DEFAULT_TIMEOUT_MILLIS, parser); - } - - /** - * @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. - * @param parser Parses the object from the network response. - */ - public NetworkLoadable(String url, String userAgent, int timeoutMillis, Parser parser) { - this.url = url; - this.userAgent = userAgent; - this.timeoutMillis = timeoutMillis; + public NetworkLoadable(String url, HttpDataSource httpDataSource, Parser parser) { + this.httpDataSource = httpDataSource; this.parser = parser; + dataSpec = new DataSpec(Uri.parse(url)); } /** @@ -103,28 +87,13 @@ public final class NetworkLoadable implements Loadable { @Override public final void load() throws IOException, InterruptedException { - String inputEncoding; - InputStream inputStream = null; + DataSourceInputStream inputStream = new DataSourceInputStream(httpDataSource, dataSpec); try { - URLConnection connection = configureConnection(new URL(url)); - inputStream = connection.getInputStream(); - inputEncoding = connection.getContentEncoding(); - result = parser.parse(connection.getURL().toString(), inputStream, inputEncoding); + inputStream.open(); + result = parser.parse(httpDataSource.getUrl(), inputStream); } finally { - if (inputStream != null) { - inputStream.close(); - } + inputStream.close(); } } - 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; - } - } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/UriDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/UriDataSource.java index 0655381191..42500bf45c 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/UriDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/UriDataSource.java @@ -37,14 +37,14 @@ public final class UriDataSource implements DataSource { /** * Constructs a new data source that delegates to a {@link FileDataSource} for file URIs and an - * {@link HttpDataSource} for other URIs. + * {@link DefaultHttpDataSource} for other URIs. * * @param userAgent The User-Agent string that should be used when requesting remote data. * @param transferListener An optional listener. */ public UriDataSource(String userAgent, TransferListener transferListener) { this(new FileDataSource(transferListener), - new HttpDataSource(userAgent, null, transferListener)); + new DefaultHttpDataSource(userAgent, null, transferListener)); } /** 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 9251ab27c0..de856b3d2a 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,8 +15,10 @@ */ package com.google.android.exoplayer.util; +import com.google.android.exoplayer.upstream.HttpDataSource; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader.Loadable; +import com.google.android.exoplayer.upstream.NetworkLoadable; import android.os.Handler; import android.os.Looper; @@ -28,6 +30,18 @@ import java.util.concurrent.CancellationException; /** * Performs both single and repeated loads of media manifests. + *

+ * 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: + *

    + *
  1. Create an instance.
  2. + *
  3. Obtain an initial manifest by calling {@link #singleLoad(Looper, ManifestCallback)} and + * waiting for the callback to be invoked.
  4. + *
  5. 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.
  6. + *
* * @param The type of manifest. */ @@ -70,7 +84,7 @@ public class ManifestFetcher implements Loader.Callback { } private final NetworkLoadable.Parser parser; - private final String userAgent; + private final HttpDataSource httpDataSource; private final Handler eventHandler; private final EventListener eventListener; @@ -89,26 +103,27 @@ public class ManifestFetcher implements Loader.Callback { /** * @param manifestUrl The manifest location. - * @param userAgent The User-Agent string that should be used. + * @param httpDataSource The {@link HttpDataSource} to use when loading the manifest. * @param parser A parser to parse the loaded manifest data. */ - public ManifestFetcher(String manifestUrl, String userAgent, NetworkLoadable.Parser parser) { - this(manifestUrl, userAgent, parser, null, null); + public ManifestFetcher(String manifestUrl, HttpDataSource httpDataSource, + NetworkLoadable.Parser parser) { + this(manifestUrl, httpDataSource, parser, null, null); } /** * @param manifestUrl The manifest location. - * @param userAgent The User-Agent string that should be used. + * @param httpDataSource The {@link HttpDataSource} 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 manifestUrl, String userAgent, NetworkLoadable.Parser parser, - Handler eventHandler, EventListener eventListener) { + public ManifestFetcher(String manifestUrl, HttpDataSource httpDataSource, + NetworkLoadable.Parser parser, Handler eventHandler, EventListener eventListener) { this.parser = parser; this.manifestUrl = manifestUrl; - this.userAgent = userAgent; + this.httpDataSource = httpDataSource; this.eventHandler = eventHandler; this.eventListener = eventListener; } @@ -131,7 +146,7 @@ public class ManifestFetcher implements Loader.Callback { */ public void singleLoad(Looper callbackLooper, final ManifestCallback callback) { SingleFetchHelper fetchHelper = new SingleFetchHelper( - new NetworkLoadable(manifestUrl, userAgent, parser), callbackLooper, callback); + new NetworkLoadable(manifestUrl, httpDataSource, parser), callbackLooper, callback); fetchHelper.startLoading(); } @@ -204,7 +219,7 @@ public class ManifestFetcher implements Loader.Callback { loader = new Loader("manifestLoader"); } if (!loader.isLoading()) { - currentLoadable = new NetworkLoadable(manifestUrl, userAgent, parser); + currentLoadable = new NetworkLoadable(manifestUrl, httpDataSource, parser); loader.startLoading(currentLoadable, this); notifyManifestRefreshStarted(); }