From d6ebd49c6320f058afa7ceda3cd135e86131d6c8 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 25 Feb 2015 11:37:39 +0000 Subject: [PATCH 1/5] Two small fixes for HLS. 1. Clear prefixFlags when a NAL unit is found. 2. continueBuffering should return true if loading is finished. --- .../java/com/google/android/exoplayer/hls/HlsSampleSource.java | 2 +- .../main/java/com/google/android/exoplayer/mp4/Mp4Util.java | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) 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 From 0fdcb3347c3cec744c145489ad827f921a08c738 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 25 Feb 2015 11:48:55 +0000 Subject: [PATCH 2/5] continueBuffering should return true when ended. This issue didn't have any material impact on playbacks, but fixing it anyway to be technically correct. --- .../android/exoplayer/chunk/ChunkSampleSource.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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; } From d2da3bbf8a2dd5af202c89f2817ab1daa14ae613 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 25 Feb 2015 12:09:06 +0000 Subject: [PATCH 3/5] Simplify NetworkLoader/Parser/ManifestParser The only downside of this change is that MediaPresentationDescriptionParser is no longer stateless. --- .../exoplayer/demo/PlayerActivity.java | 6 +- .../demo/player/DashRendererBuilder.java | 13 ++-- .../demo/player/HlsRendererBuilder.java | 10 +-- .../SmoothStreamingRendererBuilder.java | 14 ++-- .../MediaPresentationDescriptionParser.java | 45 +++++++---- .../dash/mpd/UtcTimingElementResolver.java | 56 +++++++------- .../android/exoplayer/hls/HlsChunkSource.java | 14 ++-- .../exoplayer/hls/HlsPlaylistParser.java | 10 ++- .../SmoothStreamingManifestParser.java | 13 ++-- .../exoplayer/util/ManifestFetcher.java | 74 +++++++------------ .../exoplayer/util/ManifestParser.java | 48 ------------ .../exoplayer/util/NetworkLoadable.java | 47 +++++++----- 12 files changed, 151 insertions(+), 199 deletions(-) delete mode 100644 library/src/main/java/com/google/android/exoplayer/util/ManifestParser.java 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..ab0c316697 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 @@ -93,7 +93,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; @@ -105,11 +104,10 @@ public class DashRendererBuilder implements RendererBuilder, 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,13 +118,12 @@ public class DashRendererBuilder implements RendererBuilder, this.player = player; this.callback = callback; MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); - manifestFetcher = new ManifestFetcher(parser, contentId, url, - userAgent); + manifestFetcher = new ManifestFetcher(url, userAgent, 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, @@ -137,7 +134,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..d0c8465bbe 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 @@ -45,15 +45,13 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback playlistFetcher = - new ManifestFetcher(parser, contentId, url, userAgent); + new ManifestFetcher(url, userAgent, 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..1d22f6cc07 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 @@ -70,7 +70,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 +77,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 +90,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", userAgent, + 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/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java index 67806bbb68..2b969a7165 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 @@ -22,8 +22,8 @@ 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.util.Assertions; -import com.google.android.exoplayer.util.ManifestParser; 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; @@ -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,8 +73,8 @@ 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, + String inputEncoding) throws IOException, ParserException { try { XmlPullParser xpp = xmlParserFactory.newPullParser(); xpp.setInput(inputStream, inputEncoding); @@ -69,7 +83,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler 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/UtcTimingElementResolver.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/UtcTimingElementResolver.java index 3c457c6fd7..1220bdd89d 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 @@ -62,16 +62,13 @@ 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 UtcTimingElement timingElement; private final long timingElementElapsedRealtime; private final UtcTimingCallback callback; private Loader singleUseLoader; - private HttpTimestampLoadable singleUseLoadable; + private NetworkLoadable singleUseLoadable; /** * Resolves a {@link UtcTimingElement}. @@ -102,10 +99,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 +119,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, userAgent, parser); singleUseLoader.startLoading(singleUseLoadable, this); } @@ -150,32 +147,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) + public Long parse(String connectionUrl, InputStream inputStream, String inputEncoding) throws ParserException, IOException { - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - String firstLine = reader.readLine(); + 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, String inputEncoding) + 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..f2469f294f 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), null); 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..d9d1e6c415 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.util.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,8 +84,9 @@ public final class HlsPlaylistParser implements ManifestParser { Pattern.compile(IV_ATTR + "=([^,.*]+)"); @Override - public HlsPlaylist parse(InputStream inputStream, String inputEncoding, - String contentId, Uri baseUri) throws IOException { + public HlsPlaylist parse(String connectionUrl, InputStream inputStream, String inputEncoding) + throws IOException, ParserException { + Uri baseUri = Util.parseBaseUri(connectionUrl); BufferedReader reader = new BufferedReader((inputEncoding == null) ? new InputStreamReader(inputStream) : new InputStreamReader(inputStream, inputEncoding)); Queue extraLines = new LinkedList(); 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..47a90da919 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 @@ -21,8 +21,9 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.Stre import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement; 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.NetworkLoadable; +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 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 String userAgent; private final Handler eventHandler; private final EventListener eventListener; @@ -83,7 +78,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,21 +87,26 @@ 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 userAgent The User-Agent string that should be used. + * @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); } /** - * @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 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, String userAgent, NetworkLoadable.Parser parser, + Handler eventHandler, EventListener eventListener) { this.parser = parser; - this.contentId = contentId; this.manifestUrl = manifestUrl; this.userAgent = userAgent; this.eventHandler = eventHandler; @@ -130,7 +130,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, userAgent, parser), callbackLooper, callback); fetchHelper.startLoading(); } @@ -203,7 +204,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, userAgent, parser); loader.startLoading(currentLoadable, this); notifyManifestRefreshStarted(); } @@ -287,16 +288,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 +310,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 +321,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 +330,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 +342,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 index 676e2380ef..9e38795e7d 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/NetworkLoadable.java +++ b/library/src/main/java/com/google/android/exoplayer/util/NetworkLoadable.java @@ -28,13 +28,34 @@ import java.net.URLConnection; * * @param The type of the object being loaded. */ -public abstract class NetworkLoadable implements Loadable { +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. + * @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; + + } public static final int DEFAULT_TIMEOUT_MILLIS = 10000; private final String url; private final String userAgent; private final int timeoutMillis; + private final Parser parser; private volatile T result; private volatile boolean isCanceled; @@ -42,20 +63,23 @@ public abstract 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 parser Parses the object from the network response. */ - public NetworkLoadable(String url, String userAgent) { - this(url, userAgent, DEFAULT_TIMEOUT_MILLIS); + 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) { + public NetworkLoadable(String url, String userAgent, int timeoutMillis, Parser parser) { this.url = url; this.userAgent = userAgent; this.timeoutMillis = timeoutMillis; + this.parser = parser; } /** @@ -85,7 +109,7 @@ public abstract class NetworkLoadable implements Loadable { URLConnection connection = configureConnection(new URL(url)); inputStream = connection.getInputStream(); inputEncoding = connection.getContentEncoding(); - result = parse(connection.getURL().toString(), inputStream, inputEncoding); + result = parser.parse(connection.getURL().toString(), inputStream, inputEncoding); } finally { if (inputStream != null) { inputStream.close(); @@ -93,19 +117,6 @@ public abstract class NetworkLoadable implements Loadable { } } - /** - * 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); From 2ce17b601fa5b46f9c9d63a93d123676a06ea853 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 25 Feb 2015 13:34:31 +0000 Subject: [PATCH 4/5] Correctly bound search into segment indices. The return value here assumed that the time being searched for was beyond the start time of the last segment. This fix also handles the case where the time is prior to the start of the first segment. --- .../com/google/android/exoplayer/dash/mpd/SegmentBase.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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; } } From 006986cc58bae6758cd75ea7bc21a0da4eae9646 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 25 Feb 2015 13:39:15 +0000 Subject: [PATCH 5/5] 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(); }