diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index 6f04db8991..e52bb377fd 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -206,13 +206,13 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, String userAgent = DemoUtil.getUserAgent(this); switch (contentType) { case DemoUtil.TYPE_SS: - return new SmoothStreamingRendererBuilder(userAgent, contentUri.toString(), contentId, + return new SmoothStreamingRendererBuilder(userAgent, contentUri.toString(), new SmoothStreamingTestMediaDrmCallback(), debugTextView); case DemoUtil.TYPE_DASH: - return new DashRendererBuilder(userAgent, contentUri.toString(), contentId, + return new DashRendererBuilder(userAgent, contentUri.toString(), new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities); case DemoUtil.TYPE_HLS: - return new HlsRendererBuilder(userAgent, contentUri.toString(), contentId); + return new HlsRendererBuilder(userAgent, contentUri.toString()); default: return new DefaultRendererBuilder(this, contentUri, debugTextView); } 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 dd1cf829d9..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; @@ -93,7 +95,6 @@ public class DashRendererBuilder implements RendererBuilder, private final String userAgent; private final String url; - private final String contentId; private final MediaDrmCallback drmCallback; private final TextView debugTextView; private final AudioCapabilities audioCapabilities; @@ -101,15 +102,15 @@ public class DashRendererBuilder implements RendererBuilder, private DemoPlayer player; private RendererBuilderCallback callback; private ManifestFetcher manifestFetcher; + private HttpDataSource manifestDataSource; private MediaPresentationDescription manifest; private long elapsedRealtimeOffset; - public DashRendererBuilder(String userAgent, String url, String contentId, - MediaDrmCallback drmCallback, TextView debugTextView, AudioCapabilities audioCapabilities) { + public DashRendererBuilder(String userAgent, String url, MediaDrmCallback drmCallback, + TextView debugTextView, AudioCapabilities audioCapabilities) { this.userAgent = userAgent; this.url = url; - this.contentId = contentId; this.drmCallback = drmCallback; this.debugTextView = debugTextView; this.audioCapabilities = audioCapabilities; @@ -120,16 +121,17 @@ public class DashRendererBuilder implements RendererBuilder, this.player = player; this.callback = callback; MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); - manifestFetcher = new ManifestFetcher(parser, contentId, url, - userAgent); + manifestDataSource = new DefaultHttpDataSource(userAgent, null); + manifestFetcher = new ManifestFetcher(url, manifestDataSource, + parser); manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); } @Override - public void onManifest(String contentId, MediaPresentationDescription manifest) { + 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(); @@ -137,7 +139,7 @@ public class DashRendererBuilder implements RendererBuilder, } @Override - public void onManifestError(String contentId, IOException e) { + public void onSingleManifestError(IOException e) { callback.onRenderersError(e); } 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 326a0689d5..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; @@ -45,15 +46,13 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback playlistFetcher = - new ManifestFetcher(parser, contentId, url, userAgent); + new ManifestFetcher(url, new DefaultHttpDataSource(userAgent, null), parser); playlistFetcher.singleLoad(player.getMainHandler().getLooper(), this); } @Override - public void onManifestError(String contentId, IOException e) { + public void onSingleManifestError(IOException e) { callback.onRenderersError(e); } @Override - public void onManifest(String contentId, HlsPlaylist manifest) { + public void onSingleManifest(HlsPlaylist manifest) { DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); DataSource dataSource = new UriDataSource(userAgent, bandwidthMeter); 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 22a342bfc7..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; @@ -70,7 +71,6 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, private final String userAgent; private final String url; - private final String contentId; private final MediaDrmCallback drmCallback; private final TextView debugTextView; @@ -78,11 +78,10 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, private RendererBuilderCallback callback; private ManifestFetcher manifestFetcher; - public SmoothStreamingRendererBuilder(String userAgent, String url, String contentId, - MediaDrmCallback drmCallback, TextView debugTextView) { + public SmoothStreamingRendererBuilder(String userAgent, String url, MediaDrmCallback drmCallback, + TextView debugTextView) { this.userAgent = userAgent; this.url = url; - this.contentId = contentId; this.drmCallback = drmCallback; this.debugTextView = debugTextView; } @@ -92,18 +91,18 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, this.player = player; this.callback = callback; SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); - manifestFetcher = new ManifestFetcher(parser, contentId, - url + "/Manifest", userAgent); + manifestFetcher = new ManifestFetcher(url + "/Manifest", + new DefaultHttpDataSource(userAgent, null), parser); manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); } @Override - public void onManifestError(String contentId, IOException exception) { + public void onSingleManifestError(IOException exception) { callback.onRenderersError(exception); } @Override - public void onManifest(String contentId, SmoothStreamingManifest manifest) { + public void onSingleManifest(SmoothStreamingManifest manifest) { Handler mainHandler = player.getMainHandler(); LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player); diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index c51be31c5c..78bb18f2b6 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java @@ -276,13 +276,14 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { boolean haveSamples = false; if (isPendingReset() || mediaChunks.isEmpty()) { // No sample available. - } else if (mediaChunks.getFirst().sampleAvailable()) { + } else if (sampleAvailableOrFinishedLastChunk(mediaChunks.getFirst())) { // There's a sample available to be read from the current chunk. haveSamples = true; } else { // It may be the case that the current chunk has been fully read but not yet discarded and // that the next chunk has an available sample. Return true if so, otherwise false. - haveSamples = mediaChunks.size() > 1 && mediaChunks.get(1).sampleAvailable(); + haveSamples = mediaChunks.size() > 1 + && sampleAvailableOrFinishedLastChunk(mediaChunks.get(1)); } if (!haveSamples) { @@ -716,6 +717,10 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { return true; } + private boolean sampleAvailableOrFinishedLastChunk(MediaChunk chunk) throws IOException { + return chunk.sampleAvailable() || (chunk.isLastChunk() && chunk.isReadFinished()); + } + private boolean isMediaChunk(Chunk chunk) { return chunk instanceof MediaChunk; } 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 67806bbb68..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,8 +21,8 @@ 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.ManifestParser; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.Util; @@ -44,11 +44,25 @@ import java.util.List; * A parser of media presentation description files. */ public class MediaPresentationDescriptionParser extends DefaultHandler - implements ManifestParser { + implements NetworkLoadable.Parser { + private final String contentId; private final XmlPullParserFactory xmlParserFactory; + /** + * Equivalent to calling {@code new MediaPresentationDescriptionParser(null)}. + */ public MediaPresentationDescriptionParser() { + this(null); + } + + /** + * @param contentId An optional content identifier to include in the parsed manifest. + */ + // TODO: Remove the need to inject a content identifier here, by not including it in the parsed + // manifest. Instead, it should be injected directly where needed (i.e. DashChunkSource). + public MediaPresentationDescriptionParser(String contentId) { + this.contentId = contentId; try { xmlParserFactory = XmlPullParserFactory.newInstance(); } catch (XmlPullParserException e) { @@ -59,17 +73,17 @@ public class MediaPresentationDescriptionParser extends DefaultHandler // MPD parsing. @Override - public MediaPresentationDescription parse(InputStream inputStream, String inputEncoding, - String contentId, Uri baseUrl) 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( "inputStream does not contain a valid media presentation description"); } - return parseMediaPresentationDescription(xpp, contentId, baseUrl); + return parseMediaPresentationDescription(xpp, Util.parseBaseUri(connectionUrl)); } catch (XmlPullParserException e) { throw new ParserException(e); } catch (ParseException e) { @@ -78,7 +92,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } protected MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp, - String contentId, Uri baseUrl) throws XmlPullParserException, IOException, ParseException { + Uri baseUrl) throws XmlPullParserException, IOException, ParseException { long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", -1); long durationMs = parseDuration(xpp, "mediaPresentationDuration", -1); long minBufferTimeMs = parseDuration(xpp, "minBufferTime", -1); @@ -97,7 +111,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } else if (isStartTag(xpp, "UTCTiming")) { utcTiming = parseUtcTiming(xpp); } else if (isStartTag(xpp, "Period")) { - periods.add(parsePeriod(xpp, contentId, baseUrl, durationMs)); + periods.add(parsePeriod(xpp, baseUrl, durationMs)); } } while (!isEndTag(xpp, "MPD")); @@ -123,7 +137,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler return new UtcTimingElement(schemeIdUri, value); } - protected Period parsePeriod(XmlPullParser xpp, String contentId, Uri baseUrl, long mpdDurationMs) + protected Period parsePeriod(XmlPullParser xpp, Uri baseUrl, long mpdDurationMs) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); long startMs = parseDuration(xpp, "start", 0); @@ -135,7 +149,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler if (isStartTag(xpp, "BaseURL")) { baseUrl = parseBaseUrl(xpp, baseUrl); } else if (isStartTag(xpp, "AdaptationSet")) { - adaptationSets.add(parseAdaptationSet(xpp, contentId, baseUrl, startMs, durationMs, + adaptationSets.add(parseAdaptationSet(xpp, baseUrl, startMs, durationMs, segmentBase)); } else if (isStartTag(xpp, "SegmentBase")) { segmentBase = parseSegmentBase(xpp, baseUrl, null); @@ -156,9 +170,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler // AdaptationSet parsing. - protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, Uri baseUrl, - long periodStartMs, long periodDurationMs, SegmentBase segmentBase) - throws XmlPullParserException, IOException { + protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, Uri baseUrl, long periodStartMs, + long periodDurationMs, SegmentBase segmentBase) throws XmlPullParserException, IOException { String mimeType = xpp.getAttributeValue(null, "mimeType"); String language = xpp.getAttributeValue(null, "lang"); @@ -181,7 +194,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler contentType = checkAdaptationSetTypeConsistency(contentType, parseAdaptationSetType(xpp.getAttributeValue(null, "contentType"))); } else if (isStartTag(xpp, "Representation")) { - Representation representation = parseRepresentation(xpp, contentId, baseUrl, periodStartMs, + Representation representation = parseRepresentation(xpp, baseUrl, periodStartMs, periodDurationMs, mimeType, language, segmentBase); contentType = checkAdaptationSetTypeConsistency(contentType, parseAdaptationSetTypeFromMimeType(representation.format.mimeType)); @@ -274,9 +287,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler // Representation parsing. - protected Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri baseUrl, - long periodStartMs, long periodDurationMs, String mimeType, String language, - SegmentBase segmentBase) throws XmlPullParserException, IOException { + protected Representation parseRepresentation(XmlPullParser xpp, Uri baseUrl, long periodStartMs, + long periodDurationMs, String mimeType, String language, SegmentBase segmentBase) + throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth"); int audioSamplingRate = parseInt(xpp, "audioSamplingRate"); diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java index f93ce33743..c6eec00602 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java @@ -148,7 +148,8 @@ public abstract class SegmentBase { * @see DashSegmentIndex#getSegmentNum(long) */ public int getSegmentNum(long timeUs) { - int lowIndex = getFirstSegmentNum(); + final int firstSegmentNum = getFirstSegmentNum(); + int lowIndex = firstSegmentNum; int highIndex = getLastSegmentNum(); if (segmentTimeline == null) { // All segments are of equal duration (with the possible exception of the last one). @@ -171,7 +172,7 @@ public abstract class SegmentBase { return midIndex; } } - return lowIndex - 1; + return lowIndex == firstSegmentNum ? lowIndex : highIndex; } } 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 3c457c6fd7..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,36 +63,34 @@ public class UtcTimingElementResolver implements Loader.Callback { 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 HttpDataSource httpDataSource; private final UtcTimingElement timingElement; private final long timingElementElapsedRealtime; private final UtcTimingCallback callback; private Loader singleUseLoader; - private HttpTimestampLoadable singleUseLoadable; + private NetworkLoadable singleUseLoadable; /** * 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); @@ -102,10 +101,10 @@ public class UtcTimingElementResolver implements Loader.Callback { 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); + resolveHttp(new Iso8601Parser()); } 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); + resolveHttp(new XsDateTimeParser()); } else { // Unsupported scheme. callback.onTimestampError(timingElement, new IOException("Unsupported utc timing scheme")); @@ -122,9 +121,9 @@ public class UtcTimingElementResolver implements Loader.Callback { } } - private void resolveHttp(int type) { + private void resolveHttp(NetworkLoadable.Parser parser) { singleUseLoader = new Loader("utctiming"); - singleUseLoadable = new HttpTimestampLoadable(timingElement.value, userAgent, type); + singleUseLoadable = new NetworkLoadable(timingElement.value, httpDataSource, parser); singleUseLoader.startLoading(singleUseLoadable, this); } @@ -150,32 +149,31 @@ public class UtcTimingElementResolver implements Loader.Callback { singleUseLoader.release(); } - private static class HttpTimestampLoadable extends NetworkLoadable { - - private final int type; - - public HttpTimestampLoadable(String url, String userAgent, int type) { - super(url, userAgent); - this.type = type; - } + private static class XsDateTimeParser implements NetworkLoadable.Parser { @Override - protected Long parse(String connectionUrl, InputStream inputStream, String inputEncoding) - throws ParserException, IOException { - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - String firstLine = reader.readLine(); + public Long parse(String connectionUrl, InputStream inputStream) throws ParserException, + IOException { + String firstLine = new BufferedReader(new InputStreamReader(inputStream)).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(); - } + return Util.parseXsDateTime(firstLine); + } catch (ParseException e) { + throw new ParserException(e); + } + } + + } + + private static class Iso8601Parser implements NetworkLoadable.Parser { + + @Override + 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. + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + return format.parse(firstLine).getTime(); } catch (ParseException e) { throw new ParserException(e); } 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 dc70320f0d..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 @@ -433,8 +433,8 @@ public class HlsChunkSource { private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex) { Uri mediaPlaylistUri = Util.getMergedUri(baseUri, enabledVariants[variantIndex].url); DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null); - Uri baseUri = Util.parseBaseUri(mediaPlaylistUri.toString()); - return new MediaPlaylistChunk(variantIndex, upstreamDataSource, dataSpec, baseUri); + return new MediaPlaylistChunk(variantIndex, upstreamDataSource, dataSpec, + mediaPlaylistUri.toString()); } private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv) { @@ -546,19 +546,19 @@ public class HlsChunkSource { @SuppressWarnings("hiding") /* package */ final int variantIndex; - private final Uri playlistBaseUri; + private final String playlistUrl; public MediaPlaylistChunk(int variantIndex, DataSource dataSource, DataSpec dataSpec, - Uri playlistBaseUri) { + String playlistUrl) { super(dataSource, dataSpec, scratchSpace); this.variantIndex = variantIndex; - this.playlistBaseUri = playlistBaseUri; + this.playlistUrl = playlistUrl; } @Override protected void consume(byte[] data, int limit) throws IOException { - HlsPlaylist playlist = playlistParser.parse(new ByteArrayInputStream(data, 0, limit), - null, null, playlistBaseUri); + HlsPlaylist playlist = playlistParser.parse(playlistUrl, + 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 0205364718..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,8 @@ 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.ManifestParser; +import com.google.android.exoplayer.upstream.NetworkLoadable; +import com.google.android.exoplayer.util.Util; import android.net.Uri; @@ -36,7 +37,7 @@ import java.util.regex.Pattern; /** * HLS playlists parsing logic. */ -public final class HlsPlaylistParser implements ManifestParser { +public final class HlsPlaylistParser implements NetworkLoadable.Parser { private static final String VERSION_TAG = "#EXT-X-VERSION"; @@ -83,10 +84,10 @@ public final class HlsPlaylistParser implements ManifestParser { Pattern.compile(IV_ATTR + "=([^,.*]+)"); @Override - public HlsPlaylist parse(InputStream inputStream, String inputEncoding, - String contentId, Uri baseUri) throws IOException { - BufferedReader reader = new BufferedReader((inputEncoding == null) - ? new InputStreamReader(inputStream) : new InputStreamReader(inputStream, inputEncoding)); + public HlsPlaylist parse(String connectionUrl, InputStream inputStream) + throws IOException, ParserException { + Uri baseUri = Util.parseBaseUri(connectionUrl); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); Queue extraLines = new LinkedList(); String line; try { diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java index b8fffd4c11..9db6c4ccc0 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java @@ -164,7 +164,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { if (!extractors.isEmpty()) { discardSamplesForDisabledTracks(extractors.getFirst(), downstreamPositionUs); } - return continueBufferingInternal(); + return loadingFinished || continueBufferingInternal(); } private boolean continueBufferingInternal() throws IOException { diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java index e593858f8b..ea41e3a2cf 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/Mp4Util.java @@ -171,6 +171,9 @@ public final class Mp4Util { // loop advance the index by three. } else if (data[i - 2] == 0 && data[i - 1] == 0 && data[i] == 1 && matchesType(data, i + 1, type)) { + if (prefixFlags != null) { + clearPrefixFlags(prefixFlags); + } return i - 2; } else { // There isn't a NAL prefix here, but there might be at the next position. We should 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 20aea8ad32..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,11 @@ 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.ManifestParser; import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.Util; import android.net.Uri; import android.util.Base64; @@ -45,7 +46,8 @@ import java.util.UUID; * @see * IIS Smooth Streaming Client Manifest Format */ -public class SmoothStreamingManifestParser implements ManifestParser { +public class SmoothStreamingManifestParser implements + NetworkLoadable.Parser { private final XmlPullParserFactory xmlParserFactory; @@ -58,12 +60,13 @@ public class SmoothStreamingManifestParser implements ManifestParser + * 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/upstream/NetworkLoadable.java b/library/src/main/java/com/google/android/exoplayer/upstream/NetworkLoadable.java new file mode 100644 index 0000000000..5e8058f6dd --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/upstream/NetworkLoadable.java @@ -0,0 +1,99 @@ +/* + * 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.ParserException; +import com.google.android.exoplayer.upstream.Loader.Loadable; + +import android.net.Uri; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A {@link Loadable} for loading an object over the network. + * + * @param The type of the object being loaded. + */ +public final class NetworkLoadable implements Loadable { + + /** + * Parses an object from data loaded over the network. + */ + public interface Parser { + + /** + * Parses an object from a network response. + * + * @param connectionUrl The source of the response, after any redirection. + * @param inputStream An {@link InputStream} from which the response data can be read. + * @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) throws ParserException, IOException; + + } + + private final DataSpec dataSpec; + private final HttpDataSource httpDataSource; + private final Parser parser; + + private volatile T result; + private volatile boolean isCanceled; + + /** + * @param url The url from which the object should be loaded. + * @param httpDataSource A {@link HttpDataSource} to use when loading the data. + * @param parser Parses the object from the network response. + */ + public NetworkLoadable(String url, HttpDataSource httpDataSource, Parser parser) { + this.httpDataSource = httpDataSource; + this.parser = parser; + dataSpec = new DataSpec(Uri.parse(url)); + } + + /** + * 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 { + DataSourceInputStream inputStream = new DataSourceInputStream(httpDataSource, dataSpec); + try { + inputStream.open(); + result = parser.parse(httpDataSource.getUrl(), inputStream); + } finally { + inputStream.close(); + } + } + +} 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 7841ba032d..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,9 +15,10 @@ */ package com.google.android.exoplayer.util; -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 android.os.Handler; import android.os.Looper; @@ -25,11 +26,22 @@ import android.os.SystemClock; import android.util.Pair; import java.io.IOException; -import java.io.InputStream; 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. */ @@ -58,24 +70,21 @@ public class ManifestFetcher implements Loader.Callback { /** * Invoked when the load has successfully completed. * - * @param contentId The content id of the media. * @param manifest The loaded manifest. */ - void onManifest(String contentId, T manifest); + void onSingleManifest(T manifest); /** * Invoked when the load has failed. * - * @param contentId The content id of the media. * @param e The cause of the failure. */ - void onManifestError(String contentId, IOException e); + void onSingleManifestError(IOException e); } - /* package */ final ManifestParser parser; - /* package */ final String contentId; - /* package */ final String userAgent; + private final NetworkLoadable.Parser parser; + private final HttpDataSource httpDataSource; private final Handler eventHandler; private final EventListener eventListener; @@ -83,7 +92,7 @@ public class ManifestFetcher implements Loader.Callback { private int enabledCount; private Loader loader; - private ManifestLoadable currentLoadable; + private NetworkLoadable currentLoadable; private int loadExceptionCount; private long loadExceptionTimestamp; @@ -92,23 +101,29 @@ public class ManifestFetcher implements Loader.Callback { private volatile T manifest; private volatile long manifestLoadTimestamp; - public ManifestFetcher(ManifestParser parser, String contentId, String manifestUrl, - String userAgent) { - this(parser, contentId, manifestUrl, userAgent, null, null); + /** + * @param manifestUrl The manifest location. + * @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, HttpDataSource httpDataSource, + NetworkLoadable.Parser parser) { + this(manifestUrl, httpDataSource, parser, null, null); } /** - * @param parser A parser to parse the loaded manifest data. - * @param contentId The content id of the content being loaded. May be null. * @param manifestUrl The manifest location. - * @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(ManifestParser parser, String contentId, String manifestUrl, - String userAgent, Handler eventHandler, EventListener eventListener) { + public ManifestFetcher(String manifestUrl, HttpDataSource httpDataSource, + NetworkLoadable.Parser parser, Handler eventHandler, EventListener eventListener) { this.parser = parser; - this.contentId = contentId; this.manifestUrl = manifestUrl; - this.userAgent = userAgent; + this.httpDataSource = httpDataSource; this.eventHandler = eventHandler; this.eventListener = eventListener; } @@ -130,7 +145,8 @@ public class ManifestFetcher implements Loader.Callback { * @param callback The callback to receive the result. */ public void singleLoad(Looper callbackLooper, final ManifestCallback callback) { - SingleFetchHelper fetchHelper = new SingleFetchHelper(callbackLooper, callback); + SingleFetchHelper fetchHelper = new SingleFetchHelper( + new NetworkLoadable(manifestUrl, httpDataSource, parser), callbackLooper, callback); fetchHelper.startLoading(); } @@ -203,7 +219,7 @@ public class ManifestFetcher implements Loader.Callback { loader = new Loader("manifestLoader"); } if (!loader.isLoading()) { - currentLoadable = new ManifestLoadable(manifestUrl, userAgent, contentId, parser); + currentLoadable = new NetworkLoadable(manifestUrl, httpDataSource, parser); loader.startLoading(currentLoadable, this); notifyManifestRefreshStarted(); } @@ -287,16 +303,17 @@ public class ManifestFetcher implements Loader.Callback { private class SingleFetchHelper implements Loader.Callback { + private final NetworkLoadable singleUseLoadable; private final Looper callbackLooper; private final ManifestCallback wrappedCallback; private final Loader singleUseLoader; - private final ManifestLoadable singleUseLoadable; - public SingleFetchHelper(Looper callbackLooper, ManifestCallback wrappedCallback) { + public SingleFetchHelper(NetworkLoadable singleUseLoadable, Looper callbackLooper, + ManifestCallback wrappedCallback) { + this.singleUseLoadable = singleUseLoadable; this.callbackLooper = callbackLooper; this.wrappedCallback = wrappedCallback; singleUseLoader = new Loader("manifestLoader:single"); - singleUseLoadable = new ManifestLoadable(manifestUrl, userAgent, contentId, parser); } public void startLoading() { @@ -308,7 +325,7 @@ public class ManifestFetcher implements Loader.Callback { try { T result = singleUseLoadable.getResult(); onSingleFetchCompleted(result); - wrappedCallback.onManifest(contentId, result); + wrappedCallback.onSingleManifest(result); } finally { releaseLoader(); } @@ -319,7 +336,7 @@ public class ManifestFetcher implements Loader.Callback { // This shouldn't ever happen, but handle it anyway. try { IOException exception = new IOException("Load cancelled", new CancellationException()); - wrappedCallback.onManifestError(contentId, exception); + wrappedCallback.onSingleManifestError(exception); } finally { releaseLoader(); } @@ -328,7 +345,7 @@ public class ManifestFetcher implements Loader.Callback { @Override public void onLoadError(Loadable loadable, IOException exception) { try { - wrappedCallback.onManifestError(contentId, exception); + wrappedCallback.onSingleManifestError(exception); } finally { releaseLoader(); } @@ -340,24 +357,4 @@ public class ManifestFetcher implements Loader.Callback { } - private static class ManifestLoadable extends NetworkLoadable { - - private final String contentId; - private final ManifestParser parser; - - public ManifestLoadable(String url, String userAgent, String contentId, - ManifestParser parser) { - super(url, userAgent); - this.contentId = contentId; - this.parser = parser; - } - - @Override - protected T parse(String connectionUrl, InputStream inputStream, String inputEncoding) - throws ParserException, IOException { - return parser.parse(inputStream, inputEncoding, contentId, Util.parseBaseUri(connectionUrl)); - } - - } - } diff --git a/library/src/main/java/com/google/android/exoplayer/util/ManifestParser.java b/library/src/main/java/com/google/android/exoplayer/util/ManifestParser.java deleted file mode 100644 index ba997a9f77..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/util/ManifestParser.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 android.net.Uri; - -import java.io.IOException; -import java.io.InputStream; - -/** - * Parses a manifest from an {@link InputStream}. - * - * @param The type of the manifest being parsed. - */ -public interface ManifestParser { - - /** - * Parses a manifest from an {@link InputStream}. - * - * @param inputStream The input stream to consume. - * @param inputEncoding The encoding of the input stream. May be null if the input encoding is - * unknown. - * @param contentId The content id to which the manifest corresponds. May be null. - * @param baseUri If the manifest contains relative uris, this is the uri they are relative to. - * May be null. - * @return The parsed manifest. - * @throws IOException If an error occurs reading the data. - * @throws ParserException If an error occurs parsing the data. - */ - T parse(InputStream inputStream, String inputEncoding, String contentId, Uri baseUri) - throws IOException, ParserException; - -} diff --git a/library/src/main/java/com/google/android/exoplayer/util/NetworkLoadable.java b/library/src/main/java/com/google/android/exoplayer/util/NetworkLoadable.java deleted file mode 100644 index 676e2380ef..0000000000 --- a/library/src/main/java/com/google/android/exoplayer/util/NetworkLoadable.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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 The type of the object being loaded. - */ -public abstract class NetworkLoadable 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; - } - -}