Merge pull request #321 from google/dev

dev -> dev-webm-vp9-opus
This commit is contained in:
ojw28 2015-02-25 14:04:48 +00:00
commit 7f70db9765
21 changed files with 660 additions and 639 deletions

View File

@ -206,13 +206,13 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
String userAgent = DemoUtil.getUserAgent(this); String userAgent = DemoUtil.getUserAgent(this);
switch (contentType) { switch (contentType) {
case DemoUtil.TYPE_SS: case DemoUtil.TYPE_SS:
return new SmoothStreamingRendererBuilder(userAgent, contentUri.toString(), contentId, return new SmoothStreamingRendererBuilder(userAgent, contentUri.toString(),
new SmoothStreamingTestMediaDrmCallback(), debugTextView); new SmoothStreamingTestMediaDrmCallback(), debugTextView);
case DemoUtil.TYPE_DASH: case DemoUtil.TYPE_DASH:
return new DashRendererBuilder(userAgent, contentUri.toString(), contentId, return new DashRendererBuilder(userAgent, contentUri.toString(),
new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities); new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities);
case DemoUtil.TYPE_HLS: case DemoUtil.TYPE_HLS:
return new HlsRendererBuilder(userAgent, contentUri.toString(), contentId); return new HlsRendererBuilder(userAgent, contentUri.toString());
default: default:
return new DefaultRendererBuilder(this, contentUri, debugTextView); return new DefaultRendererBuilder(this, contentUri, debugTextView);
} }

View File

@ -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.BufferPool;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; 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.upstream.UriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
@ -93,7 +95,6 @@ public class DashRendererBuilder implements RendererBuilder,
private final String userAgent; private final String userAgent;
private final String url; private final String url;
private final String contentId;
private final MediaDrmCallback drmCallback; private final MediaDrmCallback drmCallback;
private final TextView debugTextView; private final TextView debugTextView;
private final AudioCapabilities audioCapabilities; private final AudioCapabilities audioCapabilities;
@ -101,15 +102,15 @@ public class DashRendererBuilder implements RendererBuilder,
private DemoPlayer player; private DemoPlayer player;
private RendererBuilderCallback callback; private RendererBuilderCallback callback;
private ManifestFetcher<MediaPresentationDescription> manifestFetcher; private ManifestFetcher<MediaPresentationDescription> manifestFetcher;
private HttpDataSource manifestDataSource;
private MediaPresentationDescription manifest; private MediaPresentationDescription manifest;
private long elapsedRealtimeOffset; private long elapsedRealtimeOffset;
public DashRendererBuilder(String userAgent, String url, String contentId, public DashRendererBuilder(String userAgent, String url, MediaDrmCallback drmCallback,
MediaDrmCallback drmCallback, TextView debugTextView, AudioCapabilities audioCapabilities) { TextView debugTextView, AudioCapabilities audioCapabilities) {
this.userAgent = userAgent; this.userAgent = userAgent;
this.url = url; this.url = url;
this.contentId = contentId;
this.drmCallback = drmCallback; this.drmCallback = drmCallback;
this.debugTextView = debugTextView; this.debugTextView = debugTextView;
this.audioCapabilities = audioCapabilities; this.audioCapabilities = audioCapabilities;
@ -120,16 +121,17 @@ public class DashRendererBuilder implements RendererBuilder,
this.player = player; this.player = player;
this.callback = callback; this.callback = callback;
MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser();
manifestFetcher = new ManifestFetcher<MediaPresentationDescription>(parser, contentId, url, manifestDataSource = new DefaultHttpDataSource(userAgent, null);
userAgent); manifestFetcher = new ManifestFetcher<MediaPresentationDescription>(url, manifestDataSource,
parser);
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
} }
@Override @Override
public void onManifest(String contentId, MediaPresentationDescription manifest) { public void onSingleManifest(MediaPresentationDescription manifest) {
this.manifest = manifest; this.manifest = manifest;
if (manifest.dynamic && manifest.utcTiming != null) { if (manifest.dynamic && manifest.utcTiming != null) {
UtcTimingElementResolver.resolveTimingElement(userAgent, manifest.utcTiming, UtcTimingElementResolver.resolveTimingElement(manifestDataSource, manifest.utcTiming,
manifestFetcher.getManifestLoadTimestamp(), this); manifestFetcher.getManifestLoadTimestamp(), this);
} else { } else {
buildRenderers(); buildRenderers();
@ -137,7 +139,7 @@ public class DashRendererBuilder implements RendererBuilder,
} }
@Override @Override
public void onManifestError(String contentId, IOException e) { public void onSingleManifestError(IOException e) {
callback.onRenderersError(e); callback.onRenderersError(e);
} }

View File

@ -29,6 +29,7 @@ import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
import com.google.android.exoplayer.text.eia608.Eia608TrackRenderer; import com.google.android.exoplayer.text.eia608.Eia608TrackRenderer;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; 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.upstream.UriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback; import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
@ -45,15 +46,13 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
private final String userAgent; private final String userAgent;
private final String url; private final String url;
private final String contentId;
private DemoPlayer player; private DemoPlayer player;
private RendererBuilderCallback callback; private RendererBuilderCallback callback;
public HlsRendererBuilder(String userAgent, String url, String contentId) { public HlsRendererBuilder(String userAgent, String url) {
this.userAgent = userAgent; this.userAgent = userAgent;
this.url = url; this.url = url;
this.contentId = contentId;
} }
@Override @Override
@ -62,17 +61,17 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
this.callback = callback; this.callback = callback;
HlsPlaylistParser parser = new HlsPlaylistParser(); HlsPlaylistParser parser = new HlsPlaylistParser();
ManifestFetcher<HlsPlaylist> playlistFetcher = ManifestFetcher<HlsPlaylist> playlistFetcher =
new ManifestFetcher<HlsPlaylist>(parser, contentId, url, userAgent); new ManifestFetcher<HlsPlaylist>(url, new DefaultHttpDataSource(userAgent, null), parser);
playlistFetcher.singleLoad(player.getMainHandler().getLooper(), this); playlistFetcher.singleLoad(player.getMainHandler().getLooper(), this);
} }
@Override @Override
public void onManifestError(String contentId, IOException e) { public void onSingleManifestError(IOException e) {
callback.onRenderersError(e); callback.onRenderersError(e);
} }
@Override @Override
public void onManifest(String contentId, HlsPlaylist manifest) { public void onSingleManifest(HlsPlaylist manifest) {
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
DataSource dataSource = new UriDataSource(userAgent, bandwidthMeter); DataSource dataSource = new UriDataSource(userAgent, bandwidthMeter);

View File

@ -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.BufferPool;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter; 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.upstream.UriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
@ -70,7 +71,6 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
private final String userAgent; private final String userAgent;
private final String url; private final String url;
private final String contentId;
private final MediaDrmCallback drmCallback; private final MediaDrmCallback drmCallback;
private final TextView debugTextView; private final TextView debugTextView;
@ -78,11 +78,10 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
private RendererBuilderCallback callback; private RendererBuilderCallback callback;
private ManifestFetcher<SmoothStreamingManifest> manifestFetcher; private ManifestFetcher<SmoothStreamingManifest> manifestFetcher;
public SmoothStreamingRendererBuilder(String userAgent, String url, String contentId, public SmoothStreamingRendererBuilder(String userAgent, String url, MediaDrmCallback drmCallback,
MediaDrmCallback drmCallback, TextView debugTextView) { TextView debugTextView) {
this.userAgent = userAgent; this.userAgent = userAgent;
this.url = url; this.url = url;
this.contentId = contentId;
this.drmCallback = drmCallback; this.drmCallback = drmCallback;
this.debugTextView = debugTextView; this.debugTextView = debugTextView;
} }
@ -92,18 +91,18 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
this.player = player; this.player = player;
this.callback = callback; this.callback = callback;
SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser(); SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser();
manifestFetcher = new ManifestFetcher<SmoothStreamingManifest>(parser, contentId, manifestFetcher = new ManifestFetcher<SmoothStreamingManifest>(url + "/Manifest",
url + "/Manifest", userAgent); new DefaultHttpDataSource(userAgent, null), parser);
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
} }
@Override @Override
public void onManifestError(String contentId, IOException exception) { public void onSingleManifestError(IOException exception) {
callback.onRenderersError(exception); callback.onRenderersError(exception);
} }
@Override @Override
public void onManifest(String contentId, SmoothStreamingManifest manifest) { public void onSingleManifest(SmoothStreamingManifest manifest) {
Handler mainHandler = player.getMainHandler(); Handler mainHandler = player.getMainHandler();
LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE)); LoadControl loadControl = new DefaultLoadControl(new BufferPool(BUFFER_SEGMENT_SIZE));
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player);

View File

@ -276,13 +276,14 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
boolean haveSamples = false; boolean haveSamples = false;
if (isPendingReset() || mediaChunks.isEmpty()) { if (isPendingReset() || mediaChunks.isEmpty()) {
// No sample available. // 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. // There's a sample available to be read from the current chunk.
haveSamples = true; haveSamples = true;
} else { } else {
// It may be the case that the current chunk has been fully read but not yet discarded and // 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. // 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) { if (!haveSamples) {
@ -716,6 +717,10 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback {
return true; return true;
} }
private boolean sampleAvailableOrFinishedLastChunk(MediaChunk chunk) throws IOException {
return chunk.sampleAvailable() || (chunk.isLastChunk() && chunk.isReadFinished());
}
private boolean isMediaChunk(Chunk chunk) { private boolean isMediaChunk(Chunk chunk) {
return chunk instanceof MediaChunk; return chunk instanceof MediaChunk;
} }

View File

@ -21,8 +21,8 @@ import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentList;
import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTemplate; import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTemplate;
import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTimelineElement; import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTimelineElement;
import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase; 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.Assertions;
import com.google.android.exoplayer.util.ManifestParser;
import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
@ -44,11 +44,25 @@ import java.util.List;
* A parser of media presentation description files. * A parser of media presentation description files.
*/ */
public class MediaPresentationDescriptionParser extends DefaultHandler public class MediaPresentationDescriptionParser extends DefaultHandler
implements ManifestParser<MediaPresentationDescription> { implements NetworkLoadable.Parser<MediaPresentationDescription> {
private final String contentId;
private final XmlPullParserFactory xmlParserFactory; private final XmlPullParserFactory xmlParserFactory;
/**
* Equivalent to calling {@code new MediaPresentationDescriptionParser(null)}.
*/
public MediaPresentationDescriptionParser() { 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 { try {
xmlParserFactory = XmlPullParserFactory.newInstance(); xmlParserFactory = XmlPullParserFactory.newInstance();
} catch (XmlPullParserException e) { } catch (XmlPullParserException e) {
@ -59,17 +73,17 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
// MPD parsing. // MPD parsing.
@Override @Override
public MediaPresentationDescription parse(InputStream inputStream, String inputEncoding, public MediaPresentationDescription parse(String connectionUrl, InputStream inputStream)
String contentId, Uri baseUrl) throws IOException, ParserException { throws IOException, ParserException {
try { try {
XmlPullParser xpp = xmlParserFactory.newPullParser(); XmlPullParser xpp = xmlParserFactory.newPullParser();
xpp.setInput(inputStream, inputEncoding); xpp.setInput(inputStream, null);
int eventType = xpp.next(); int eventType = xpp.next();
if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) { if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) {
throw new ParserException( throw new ParserException(
"inputStream does not contain a valid media presentation description"); "inputStream does not contain a valid media presentation description");
} }
return parseMediaPresentationDescription(xpp, contentId, baseUrl); return parseMediaPresentationDescription(xpp, Util.parseBaseUri(connectionUrl));
} catch (XmlPullParserException e) { } catch (XmlPullParserException e) {
throw new ParserException(e); throw new ParserException(e);
} catch (ParseException e) { } catch (ParseException e) {
@ -78,7 +92,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
} }
protected MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp, 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 availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", -1);
long durationMs = parseDuration(xpp, "mediaPresentationDuration", -1); long durationMs = parseDuration(xpp, "mediaPresentationDuration", -1);
long minBufferTimeMs = parseDuration(xpp, "minBufferTime", -1); long minBufferTimeMs = parseDuration(xpp, "minBufferTime", -1);
@ -97,7 +111,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
} else if (isStartTag(xpp, "UTCTiming")) { } else if (isStartTag(xpp, "UTCTiming")) {
utcTiming = parseUtcTiming(xpp); utcTiming = parseUtcTiming(xpp);
} else if (isStartTag(xpp, "Period")) { } else if (isStartTag(xpp, "Period")) {
periods.add(parsePeriod(xpp, contentId, baseUrl, durationMs)); periods.add(parsePeriod(xpp, baseUrl, durationMs));
} }
} while (!isEndTag(xpp, "MPD")); } while (!isEndTag(xpp, "MPD"));
@ -123,7 +137,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
return new UtcTimingElement(schemeIdUri, value); 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 { throws XmlPullParserException, IOException {
String id = xpp.getAttributeValue(null, "id"); String id = xpp.getAttributeValue(null, "id");
long startMs = parseDuration(xpp, "start", 0); long startMs = parseDuration(xpp, "start", 0);
@ -135,7 +149,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
if (isStartTag(xpp, "BaseURL")) { if (isStartTag(xpp, "BaseURL")) {
baseUrl = parseBaseUrl(xpp, baseUrl); baseUrl = parseBaseUrl(xpp, baseUrl);
} else if (isStartTag(xpp, "AdaptationSet")) { } else if (isStartTag(xpp, "AdaptationSet")) {
adaptationSets.add(parseAdaptationSet(xpp, contentId, baseUrl, startMs, durationMs, adaptationSets.add(parseAdaptationSet(xpp, baseUrl, startMs, durationMs,
segmentBase)); segmentBase));
} else if (isStartTag(xpp, "SegmentBase")) { } else if (isStartTag(xpp, "SegmentBase")) {
segmentBase = parseSegmentBase(xpp, baseUrl, null); segmentBase = parseSegmentBase(xpp, baseUrl, null);
@ -156,9 +170,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
// AdaptationSet parsing. // AdaptationSet parsing.
protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, Uri baseUrl, protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, Uri baseUrl, long periodStartMs,
long periodStartMs, long periodDurationMs, SegmentBase segmentBase) long periodDurationMs, SegmentBase segmentBase) throws XmlPullParserException, IOException {
throws XmlPullParserException, IOException {
String mimeType = xpp.getAttributeValue(null, "mimeType"); String mimeType = xpp.getAttributeValue(null, "mimeType");
String language = xpp.getAttributeValue(null, "lang"); String language = xpp.getAttributeValue(null, "lang");
@ -181,7 +194,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
contentType = checkAdaptationSetTypeConsistency(contentType, contentType = checkAdaptationSetTypeConsistency(contentType,
parseAdaptationSetType(xpp.getAttributeValue(null, "contentType"))); parseAdaptationSetType(xpp.getAttributeValue(null, "contentType")));
} else if (isStartTag(xpp, "Representation")) { } else if (isStartTag(xpp, "Representation")) {
Representation representation = parseRepresentation(xpp, contentId, baseUrl, periodStartMs, Representation representation = parseRepresentation(xpp, baseUrl, periodStartMs,
periodDurationMs, mimeType, language, segmentBase); periodDurationMs, mimeType, language, segmentBase);
contentType = checkAdaptationSetTypeConsistency(contentType, contentType = checkAdaptationSetTypeConsistency(contentType,
parseAdaptationSetTypeFromMimeType(representation.format.mimeType)); parseAdaptationSetTypeFromMimeType(representation.format.mimeType));
@ -274,9 +287,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
// Representation parsing. // Representation parsing.
protected Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri baseUrl, protected Representation parseRepresentation(XmlPullParser xpp, Uri baseUrl, long periodStartMs,
long periodStartMs, long periodDurationMs, String mimeType, String language, long periodDurationMs, String mimeType, String language, SegmentBase segmentBase)
SegmentBase segmentBase) throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
String id = xpp.getAttributeValue(null, "id"); String id = xpp.getAttributeValue(null, "id");
int bandwidth = parseInt(xpp, "bandwidth"); int bandwidth = parseInt(xpp, "bandwidth");
int audioSamplingRate = parseInt(xpp, "audioSamplingRate"); int audioSamplingRate = parseInt(xpp, "audioSamplingRate");

View File

@ -148,7 +148,8 @@ public abstract class SegmentBase {
* @see DashSegmentIndex#getSegmentNum(long) * @see DashSegmentIndex#getSegmentNum(long)
*/ */
public int getSegmentNum(long timeUs) { public int getSegmentNum(long timeUs) {
int lowIndex = getFirstSegmentNum(); final int firstSegmentNum = getFirstSegmentNum();
int lowIndex = firstSegmentNum;
int highIndex = getLastSegmentNum(); int highIndex = getLastSegmentNum();
if (segmentTimeline == null) { if (segmentTimeline == null) {
// All segments are of equal duration (with the possible exception of the last one). // 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 midIndex;
} }
} }
return lowIndex - 1; return lowIndex == firstSegmentNum ? lowIndex : highIndex;
} }
} }

View File

@ -16,10 +16,11 @@
package com.google.android.exoplayer.dash.mpd; package com.google.android.exoplayer.dash.mpd;
import com.google.android.exoplayer.ParserException; 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;
import com.google.android.exoplayer.upstream.Loader.Loadable; 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.Assertions;
import com.google.android.exoplayer.util.NetworkLoadable;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.os.SystemClock; import android.os.SystemClock;
@ -62,36 +63,34 @@ public class UtcTimingElementResolver implements Loader.Callback {
void onTimestampError(UtcTimingElement utcTiming, IOException e); void onTimestampError(UtcTimingElement utcTiming, IOException e);
} }
private static final int TYPE_XS = 0; private final HttpDataSource httpDataSource;
private static final int TYPE_ISO = 1;
private final String userAgent;
private final UtcTimingElement timingElement; private final UtcTimingElement timingElement;
private final long timingElementElapsedRealtime; private final long timingElementElapsedRealtime;
private final UtcTimingCallback callback; private final UtcTimingCallback callback;
private Loader singleUseLoader; private Loader singleUseLoader;
private HttpTimestampLoadable singleUseLoadable; private NetworkLoadable<Long> singleUseLoadable;
/** /**
* Resolves a {@link UtcTimingElement}. * 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 timingElement The element to resolve.
* @param timingElementElapsedRealtime The {@link SystemClock#elapsedRealtime()} timestamp at * @param timingElementElapsedRealtime The {@link SystemClock#elapsedRealtime()} timestamp at
* which the element was obtained. Used if the element contains a timestamp directly. * which the element was obtained. Used if the element contains a timestamp directly.
* @param callback The callback to invoke on resolution or failure. * @param callback The callback to invoke on resolution or failure.
*/ */
public static void resolveTimingElement(String userAgent, UtcTimingElement timingElement, public static void resolveTimingElement(HttpDataSource httpDataSource,
long timingElementElapsedRealtime, UtcTimingCallback callback) { UtcTimingElement timingElement, long timingElementElapsedRealtime,
UtcTimingElementResolver resolver = new UtcTimingElementResolver(userAgent, timingElement, UtcTimingCallback callback) {
UtcTimingElementResolver resolver = new UtcTimingElementResolver(httpDataSource, timingElement,
timingElementElapsedRealtime, callback); timingElementElapsedRealtime, callback);
resolver.resolve(); resolver.resolve();
} }
private UtcTimingElementResolver(String userAgent, UtcTimingElement timingElement, private UtcTimingElementResolver(HttpDataSource httpDataSource, UtcTimingElement timingElement,
long timingElementElapsedRealtime, UtcTimingCallback callback) { long timingElementElapsedRealtime, UtcTimingCallback callback) {
this.userAgent = userAgent; this.httpDataSource = httpDataSource;
this.timingElement = Assertions.checkNotNull(timingElement); this.timingElement = Assertions.checkNotNull(timingElement);
this.timingElementElapsedRealtime = timingElementElapsedRealtime; this.timingElementElapsedRealtime = timingElementElapsedRealtime;
this.callback = Assertions.checkNotNull(callback); this.callback = Assertions.checkNotNull(callback);
@ -102,10 +101,10 @@ public class UtcTimingElementResolver implements Loader.Callback {
if (Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2012")) { if (Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2012")) {
resolveDirect(); resolveDirect();
} else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2014")) { } 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") } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012")
|| Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014")) { || Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014")) {
resolveHttp(TYPE_XS); resolveHttp(new XsDateTimeParser());
} else { } else {
// Unsupported scheme. // Unsupported scheme.
callback.onTimestampError(timingElement, new IOException("Unsupported utc timing scheme")); callback.onTimestampError(timingElement, new IOException("Unsupported utc timing scheme"));
@ -122,9 +121,9 @@ public class UtcTimingElementResolver implements Loader.Callback {
} }
} }
private void resolveHttp(int type) { private void resolveHttp(NetworkLoadable.Parser<Long> parser) {
singleUseLoader = new Loader("utctiming"); singleUseLoader = new Loader("utctiming");
singleUseLoadable = new HttpTimestampLoadable(timingElement.value, userAgent, type); singleUseLoadable = new NetworkLoadable<Long>(timingElement.value, httpDataSource, parser);
singleUseLoader.startLoading(singleUseLoadable, this); singleUseLoader.startLoading(singleUseLoadable, this);
} }
@ -150,32 +149,31 @@ public class UtcTimingElementResolver implements Loader.Callback {
singleUseLoader.release(); singleUseLoader.release();
} }
private static class HttpTimestampLoadable extends NetworkLoadable<Long> { private static class XsDateTimeParser implements NetworkLoadable.Parser<Long> {
private final int type;
public HttpTimestampLoadable(String url, String userAgent, int type) {
super(url, userAgent);
this.type = type;
}
@Override @Override
protected Long parse(String connectionUrl, InputStream inputStream, String inputEncoding) public Long parse(String connectionUrl, InputStream inputStream) throws ParserException,
throws ParserException, IOException { IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine();
String firstLine = reader.readLine();
try { try {
switch (type) { return Util.parseXsDateTime(firstLine);
case TYPE_XS: } catch (ParseException e) {
return Util.parseXsDateTime(firstLine); throw new ParserException(e);
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. private static class Iso8601Parser implements NetworkLoadable.Parser<Long> {
throw new RuntimeException();
} @Override
public Long parse(String connectionUrl, InputStream inputStream) throws ParserException,
IOException {
String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine();
try {
// TODO: It may be necessary to handle timestamp offsets from UTC.
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
return format.parse(firstLine).getTime();
} catch (ParseException e) { } catch (ParseException e) {
throw new ParserException(e); throw new ParserException(e);
} }

View File

@ -433,8 +433,8 @@ public class HlsChunkSource {
private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex) { private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex) {
Uri mediaPlaylistUri = Util.getMergedUri(baseUri, enabledVariants[variantIndex].url); Uri mediaPlaylistUri = Util.getMergedUri(baseUri, enabledVariants[variantIndex].url);
DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null); DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null);
Uri baseUri = Util.parseBaseUri(mediaPlaylistUri.toString()); return new MediaPlaylistChunk(variantIndex, upstreamDataSource, dataSpec,
return new MediaPlaylistChunk(variantIndex, upstreamDataSource, dataSpec, baseUri); mediaPlaylistUri.toString());
} }
private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv) { private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv) {
@ -546,19 +546,19 @@ public class HlsChunkSource {
@SuppressWarnings("hiding") @SuppressWarnings("hiding")
/* package */ final int variantIndex; /* package */ final int variantIndex;
private final Uri playlistBaseUri; private final String playlistUrl;
public MediaPlaylistChunk(int variantIndex, DataSource dataSource, DataSpec dataSpec, public MediaPlaylistChunk(int variantIndex, DataSource dataSource, DataSpec dataSpec,
Uri playlistBaseUri) { String playlistUrl) {
super(dataSource, dataSpec, scratchSpace); super(dataSource, dataSpec, scratchSpace);
this.variantIndex = variantIndex; this.variantIndex = variantIndex;
this.playlistBaseUri = playlistBaseUri; this.playlistUrl = playlistUrl;
} }
@Override @Override
protected void consume(byte[] data, int limit) throws IOException { protected void consume(byte[] data, int limit) throws IOException {
HlsPlaylist playlist = playlistParser.parse(new ByteArrayInputStream(data, 0, limit), HlsPlaylist playlist = playlistParser.parse(playlistUrl,
null, null, playlistBaseUri); new ByteArrayInputStream(data, 0, limit));
Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MEDIA); Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MEDIA);
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
setMediaPlaylist(variantIndex, mediaPlaylist); setMediaPlaylist(variantIndex, mediaPlaylist);

View File

@ -18,7 +18,8 @@ package com.google.android.exoplayer.hls;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.hls.HlsMediaPlaylist.Segment; import com.google.android.exoplayer.hls.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer.util.ManifestParser; import com.google.android.exoplayer.upstream.NetworkLoadable;
import com.google.android.exoplayer.util.Util;
import android.net.Uri; import android.net.Uri;
@ -36,7 +37,7 @@ import java.util.regex.Pattern;
/** /**
* HLS playlists parsing logic. * HLS playlists parsing logic.
*/ */
public final class HlsPlaylistParser implements ManifestParser<HlsPlaylist> { public final class HlsPlaylistParser implements NetworkLoadable.Parser<HlsPlaylist> {
private static final String VERSION_TAG = "#EXT-X-VERSION"; private static final String VERSION_TAG = "#EXT-X-VERSION";
@ -83,10 +84,10 @@ public final class HlsPlaylistParser implements ManifestParser<HlsPlaylist> {
Pattern.compile(IV_ATTR + "=([^,.*]+)"); Pattern.compile(IV_ATTR + "=([^,.*]+)");
@Override @Override
public HlsPlaylist parse(InputStream inputStream, String inputEncoding, public HlsPlaylist parse(String connectionUrl, InputStream inputStream)
String contentId, Uri baseUri) throws IOException { throws IOException, ParserException {
BufferedReader reader = new BufferedReader((inputEncoding == null) Uri baseUri = Util.parseBaseUri(connectionUrl);
? new InputStreamReader(inputStream) : new InputStreamReader(inputStream, inputEncoding)); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
Queue<String> extraLines = new LinkedList<String>(); Queue<String> extraLines = new LinkedList<String>();
String line; String line;
try { try {

View File

@ -164,7 +164,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
if (!extractors.isEmpty()) { if (!extractors.isEmpty()) {
discardSamplesForDisabledTracks(extractors.getFirst(), downstreamPositionUs); discardSamplesForDisabledTracks(extractors.getFirst(), downstreamPositionUs);
} }
return continueBufferingInternal(); return loadingFinished || continueBufferingInternal();
} }
private boolean continueBufferingInternal() throws IOException { private boolean continueBufferingInternal() throws IOException {

View File

@ -171,6 +171,9 @@ public final class Mp4Util {
// loop advance the index by three. // loop advance the index by three.
} else if (data[i - 2] == 0 && data[i - 1] == 0 && data[i] == 1 } else if (data[i - 2] == 0 && data[i - 1] == 0 && data[i] == 1
&& matchesType(data, i + 1, type)) { && matchesType(data, i + 1, type)) {
if (prefixFlags != null) {
clearPrefixFlags(prefixFlags);
}
return i - 2; return i - 2;
} else { } else {
// There isn't a NAL prefix here, but there might be at the next position. We should // There isn't a NAL prefix here, but there might be at the next position. We should

View File

@ -19,10 +19,11 @@ import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement; 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.Assertions;
import com.google.android.exoplayer.util.CodecSpecificDataUtil; 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.MimeTypes;
import com.google.android.exoplayer.util.Util;
import android.net.Uri; import android.net.Uri;
import android.util.Base64; import android.util.Base64;
@ -45,7 +46,8 @@ import java.util.UUID;
* @see <a href="http://msdn.microsoft.com/en-us/library/ee673436(v=vs.90).aspx"> * @see <a href="http://msdn.microsoft.com/en-us/library/ee673436(v=vs.90).aspx">
* IIS Smooth Streaming Client Manifest Format</a> * IIS Smooth Streaming Client Manifest Format</a>
*/ */
public class SmoothStreamingManifestParser implements ManifestParser<SmoothStreamingManifest> { public class SmoothStreamingManifestParser implements
NetworkLoadable.Parser<SmoothStreamingManifest> {
private final XmlPullParserFactory xmlParserFactory; private final XmlPullParserFactory xmlParserFactory;
@ -58,12 +60,13 @@ public class SmoothStreamingManifestParser implements ManifestParser<SmoothStrea
} }
@Override @Override
public SmoothStreamingManifest parse(InputStream inputStream, String inputEncoding, public SmoothStreamingManifest parse(String connectionUrl, InputStream inputStream)
String contentId, Uri baseUri) throws IOException, ParserException { throws IOException, ParserException {
try { try {
XmlPullParser xmlParser = xmlParserFactory.newPullParser(); XmlPullParser xmlParser = xmlParserFactory.newPullParser();
xmlParser.setInput(inputStream, inputEncoding); xmlParser.setInput(inputStream, null);
SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null, baseUri); SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null,
Util.parseBaseUri(connectionUrl));
return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser); return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser);
} catch (XmlPullParserException e) { } catch (XmlPullParserException e) {
throw new ParserException(e); throw new ParserException(e);

View File

@ -43,6 +43,19 @@ public class DataSourceInputStream extends InputStream {
singleByteArray = new byte[1]; singleByteArray = new byte[1];
} }
/**
* Optional call to open the underlying {@link DataSource}.
* <p>
* 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 @Override
public int read() throws IOException { public int read() throws IOException {
read(singleByteArray); read(singleByteArray);

View File

@ -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<String> contentTypePredicate;
private final HashMap<String, String> 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<String> 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<String> 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<String> contentTypePredicate,
TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) {
this.userAgent = Assertions.checkNotEmpty(userAgent);
this.contentTypePredicate = contentTypePredicate;
this.listener = listener;
this.requestProperties = new HashMap<String, String>();
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<String, List<String>> 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}.
* <p>
* 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<String, String> 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;
}
}

View File

@ -15,28 +15,20 @@
*/ */
package com.google.android.exoplayer.upstream; 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.Predicate;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import java.io.IOException; 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.List;
import java.util.Map; 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. * 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 { 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<String> contentTypePredicate;
private final HashMap<String, String> 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. * When the source is open, returns the url from which data is being read.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is * <p>
* rejected by the predicate then a {@link InvalidContentTypeException} is thrown from * If redirection occurred, the url after redirection is the one returned.
* {@link #open(DataSpec)}. *
* @return When the source is open, the url from which data is being read. Null otherwise.
*/ */
public HttpDataSource(String userAgent, Predicate<String> contentTypePredicate) { String getUrl();
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<String> 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<String> contentTypePredicate,
TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) {
this.userAgent = Assertions.checkNotEmpty(userAgent);
this.contentTypePredicate = contentTypePredicate;
this.listener = listener;
this.requestProperties = new HashMap<String, String>();
this.connectTimeoutMillis = connectTimeoutMillis;
this.readTimeoutMillis = readTimeoutMillis;
}
/** /**
* Sets the value of a request header field. The value will be used for subsequent connections * 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 name The name of the header field.
* @param value The value of the field. * @param value The value of the field.
*/ */
public void setRequestProperty(String name, String value) { void setRequestProperty(String name, String value);
Assertions.checkNotNull(name);
Assertions.checkNotNull(value);
synchronized (requestProperties) {
requestProperties.put(name, value);
}
}
/** /**
* Clears the value of a request header field. The change will apply to subsequent connections * 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. * @param name The name of the header field.
*/ */
public void clearRequestProperty(String name) { void clearRequestProperty(String name);
Assertions.checkNotNull(name);
synchronized (requestProperties) {
requestProperties.remove(name);
}
}
/** /**
* Clears all request header fields that were set by {@link #setRequestProperty(String, String)}. * Clears all request header fields that were set by {@link #setRequestProperty(String, String)}.
*/ */
public void clearAllRequestProperties() { 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<String, List<String>> 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}.
* <p>
* 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<String, String> 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;
}
} }

View File

@ -0,0 +1,99 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.upstream;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import android.net.Uri;
import java.io.IOException;
import java.io.InputStream;
/**
* A {@link Loadable} for loading an object over the network.
*
* @param <T> The type of the object being loaded.
*/
public final class NetworkLoadable<T> implements Loadable {
/**
* Parses an object from data loaded over the network.
*/
public interface Parser<T> {
/**
* Parses an object from a network response.
*
* @param connectionUrl The source of the response, after any redirection.
* @param inputStream An {@link InputStream} from which the response data can be read.
* @return The parsed object.
* @throws ParserException If an error occurs parsing the data.
* @throws IOException If an error occurs reading data from the stream.
*/
T parse(String connectionUrl, InputStream inputStream) throws ParserException, IOException;
}
private final DataSpec dataSpec;
private final HttpDataSource httpDataSource;
private final Parser<T> parser;
private volatile T result;
private volatile boolean isCanceled;
/**
* @param url The url from which the object should be loaded.
* @param httpDataSource A {@link HttpDataSource} to use when loading the data.
* @param parser Parses the object from the network response.
*/
public NetworkLoadable(String url, HttpDataSource httpDataSource, Parser<T> parser) {
this.httpDataSource = httpDataSource;
this.parser = parser;
dataSpec = new DataSpec(Uri.parse(url));
}
/**
* Returns the loaded object, or null if an object has not been loaded.
*/
public final T getResult() {
return result;
}
@Override
public final void cancelLoad() {
// We don't actually cancel anything, but we need to record the cancellation so that
// isLoadCanceled can return the correct value.
isCanceled = true;
}
@Override
public final boolean isLoadCanceled() {
return isCanceled;
}
@Override
public final void load() throws IOException, InterruptedException {
DataSourceInputStream inputStream = new DataSourceInputStream(httpDataSource, dataSpec);
try {
inputStream.open();
result = parser.parse(httpDataSource.getUrl(), inputStream);
} finally {
inputStream.close();
}
}
}

View File

@ -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 * 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 userAgent The User-Agent string that should be used when requesting remote data.
* @param transferListener An optional listener. * @param transferListener An optional listener.
*/ */
public UriDataSource(String userAgent, TransferListener transferListener) { public UriDataSource(String userAgent, TransferListener transferListener) {
this(new FileDataSource(transferListener), this(new FileDataSource(transferListener),
new HttpDataSource(userAgent, null, transferListener)); new DefaultHttpDataSource(userAgent, null, transferListener));
} }
/** /**

View File

@ -15,9 +15,10 @@
*/ */
package com.google.android.exoplayer.util; package com.google.android.exoplayer.util;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.upstream.HttpDataSource;
import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.upstream.NetworkLoadable;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
@ -25,11 +26,22 @@ import android.os.SystemClock;
import android.util.Pair; import android.util.Pair;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
/** /**
* Performs both single and repeated loads of media manifests. * Performs both single and repeated loads of media manifests.
* <p>
* 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:
* <ol>
* <li>Create an instance.</li>
* <li>Obtain an initial manifest by calling {@link #singleLoad(Looper, ManifestCallback)} and
* waiting for the callback to be invoked.</li>
* <li>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.</li>
* </ol>
* *
* @param <T> The type of manifest. * @param <T> The type of manifest.
*/ */
@ -58,24 +70,21 @@ public class ManifestFetcher<T> implements Loader.Callback {
/** /**
* Invoked when the load has successfully completed. * Invoked when the load has successfully completed.
* *
* @param contentId The content id of the media.
* @param manifest The loaded manifest. * @param manifest The loaded manifest.
*/ */
void onManifest(String contentId, T manifest); void onSingleManifest(T manifest);
/** /**
* Invoked when the load has failed. * Invoked when the load has failed.
* *
* @param contentId The content id of the media.
* @param e The cause of the failure. * @param e The cause of the failure.
*/ */
void onManifestError(String contentId, IOException e); void onSingleManifestError(IOException e);
} }
/* package */ final ManifestParser<T> parser; private final NetworkLoadable.Parser<T> parser;
/* package */ final String contentId; private final HttpDataSource httpDataSource;
/* package */ final String userAgent;
private final Handler eventHandler; private final Handler eventHandler;
private final EventListener eventListener; private final EventListener eventListener;
@ -83,7 +92,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
private int enabledCount; private int enabledCount;
private Loader loader; private Loader loader;
private ManifestLoadable<T> currentLoadable; private NetworkLoadable<T> currentLoadable;
private int loadExceptionCount; private int loadExceptionCount;
private long loadExceptionTimestamp; private long loadExceptionTimestamp;
@ -92,23 +101,29 @@ public class ManifestFetcher<T> implements Loader.Callback {
private volatile T manifest; private volatile T manifest;
private volatile long manifestLoadTimestamp; private volatile long manifestLoadTimestamp;
public ManifestFetcher(ManifestParser<T> parser, String contentId, String manifestUrl, /**
String userAgent) { * @param manifestUrl The manifest location.
this(parser, contentId, manifestUrl, userAgent, null, null); * @param httpDataSource The {@link HttpDataSource} to use when loading the manifest.
* @param parser A parser to parse the loaded manifest data.
*/
public ManifestFetcher(String manifestUrl, HttpDataSource httpDataSource,
NetworkLoadable.Parser<T> parser) {
this(manifestUrl, httpDataSource, parser, null, null);
} }
/** /**
* @param parser A parser to parse the loaded manifest data.
* @param contentId The content id of the content being loaded. May be null.
* @param manifestUrl The manifest location. * @param manifestUrl The manifest location.
* @param userAgent The User-Agent string that should be used. * @param httpDataSource The {@link HttpDataSource} to use when loading the manifest.
* @param parser A parser to parse the loaded manifest data.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
*/ */
public ManifestFetcher(ManifestParser<T> parser, String contentId, String manifestUrl, public ManifestFetcher(String manifestUrl, HttpDataSource httpDataSource,
String userAgent, Handler eventHandler, EventListener eventListener) { NetworkLoadable.Parser<T> parser, Handler eventHandler, EventListener eventListener) {
this.parser = parser; this.parser = parser;
this.contentId = contentId;
this.manifestUrl = manifestUrl; this.manifestUrl = manifestUrl;
this.userAgent = userAgent; this.httpDataSource = httpDataSource;
this.eventHandler = eventHandler; this.eventHandler = eventHandler;
this.eventListener = eventListener; this.eventListener = eventListener;
} }
@ -130,7 +145,8 @@ public class ManifestFetcher<T> implements Loader.Callback {
* @param callback The callback to receive the result. * @param callback The callback to receive the result.
*/ */
public void singleLoad(Looper callbackLooper, final ManifestCallback<T> callback) { public void singleLoad(Looper callbackLooper, final ManifestCallback<T> callback) {
SingleFetchHelper fetchHelper = new SingleFetchHelper(callbackLooper, callback); SingleFetchHelper fetchHelper = new SingleFetchHelper(
new NetworkLoadable<T>(manifestUrl, httpDataSource, parser), callbackLooper, callback);
fetchHelper.startLoading(); fetchHelper.startLoading();
} }
@ -203,7 +219,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
loader = new Loader("manifestLoader"); loader = new Loader("manifestLoader");
} }
if (!loader.isLoading()) { if (!loader.isLoading()) {
currentLoadable = new ManifestLoadable<T>(manifestUrl, userAgent, contentId, parser); currentLoadable = new NetworkLoadable<T>(manifestUrl, httpDataSource, parser);
loader.startLoading(currentLoadable, this); loader.startLoading(currentLoadable, this);
notifyManifestRefreshStarted(); notifyManifestRefreshStarted();
} }
@ -287,16 +303,17 @@ public class ManifestFetcher<T> implements Loader.Callback {
private class SingleFetchHelper implements Loader.Callback { private class SingleFetchHelper implements Loader.Callback {
private final NetworkLoadable<T> singleUseLoadable;
private final Looper callbackLooper; private final Looper callbackLooper;
private final ManifestCallback<T> wrappedCallback; private final ManifestCallback<T> wrappedCallback;
private final Loader singleUseLoader; private final Loader singleUseLoader;
private final ManifestLoadable<T> singleUseLoadable;
public SingleFetchHelper(Looper callbackLooper, ManifestCallback<T> wrappedCallback) { public SingleFetchHelper(NetworkLoadable<T> singleUseLoadable, Looper callbackLooper,
ManifestCallback<T> wrappedCallback) {
this.singleUseLoadable = singleUseLoadable;
this.callbackLooper = callbackLooper; this.callbackLooper = callbackLooper;
this.wrappedCallback = wrappedCallback; this.wrappedCallback = wrappedCallback;
singleUseLoader = new Loader("manifestLoader:single"); singleUseLoader = new Loader("manifestLoader:single");
singleUseLoadable = new ManifestLoadable<T>(manifestUrl, userAgent, contentId, parser);
} }
public void startLoading() { public void startLoading() {
@ -308,7 +325,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
try { try {
T result = singleUseLoadable.getResult(); T result = singleUseLoadable.getResult();
onSingleFetchCompleted(result); onSingleFetchCompleted(result);
wrappedCallback.onManifest(contentId, result); wrappedCallback.onSingleManifest(result);
} finally { } finally {
releaseLoader(); releaseLoader();
} }
@ -319,7 +336,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
// This shouldn't ever happen, but handle it anyway. // This shouldn't ever happen, but handle it anyway.
try { try {
IOException exception = new IOException("Load cancelled", new CancellationException()); IOException exception = new IOException("Load cancelled", new CancellationException());
wrappedCallback.onManifestError(contentId, exception); wrappedCallback.onSingleManifestError(exception);
} finally { } finally {
releaseLoader(); releaseLoader();
} }
@ -328,7 +345,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
@Override @Override
public void onLoadError(Loadable loadable, IOException exception) { public void onLoadError(Loadable loadable, IOException exception) {
try { try {
wrappedCallback.onManifestError(contentId, exception); wrappedCallback.onSingleManifestError(exception);
} finally { } finally {
releaseLoader(); releaseLoader();
} }
@ -340,24 +357,4 @@ public class ManifestFetcher<T> implements Loader.Callback {
} }
private static class ManifestLoadable<T> extends NetworkLoadable<T> {
private final String contentId;
private final ManifestParser<T> parser;
public ManifestLoadable(String url, String userAgent, String contentId,
ManifestParser<T> 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));
}
}
} }

View File

@ -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 <T> The type of the manifest being parsed.
*/
public interface ManifestParser<T> {
/**
* 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;
}

View File

@ -1,119 +0,0 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.util;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
/**
* A {@link Loadable} for loading an object over the network.
*
* @param <T> The type of the object being loaded.
*/
public abstract class NetworkLoadable<T> implements Loadable {
public static final int DEFAULT_TIMEOUT_MILLIS = 10000;
private final String url;
private final String userAgent;
private final int timeoutMillis;
private volatile T result;
private volatile boolean isCanceled;
/**
* @param url The url from which the object should be loaded.
* @param userAgent The user agent to use when requesting the object.
*/
public NetworkLoadable(String url, String userAgent) {
this(url, userAgent, DEFAULT_TIMEOUT_MILLIS);
}
/**
* @param url The url from which the object should be loaded.
* @param userAgent The user agent to use when requesting the object.
* @param timeoutMillis The desired http timeout in milliseconds.
*/
public NetworkLoadable(String url, String userAgent, int timeoutMillis) {
this.url = url;
this.userAgent = userAgent;
this.timeoutMillis = timeoutMillis;
}
/**
* Returns the loaded object, or null if an object has not been loaded.
*/
public final T getResult() {
return result;
}
@Override
public final void cancelLoad() {
// We don't actually cancel anything, but we need to record the cancellation so that
// isLoadCanceled can return the correct value.
isCanceled = true;
}
@Override
public final boolean isLoadCanceled() {
return isCanceled;
}
@Override
public final void load() throws IOException, InterruptedException {
String inputEncoding;
InputStream inputStream = null;
try {
URLConnection connection = configureConnection(new URL(url));
inputStream = connection.getInputStream();
inputEncoding = connection.getContentEncoding();
result = parse(connection.getURL().toString(), inputStream, inputEncoding);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
/**
* Parses the raw data into an object.
*
* @param connectionUrl The url, after any redirection has taken place.
* @param inputStream An {@link InputStream} from which the raw data can be read.
* @param inputEncoding The encoding of the raw data, if available.
* @return The parsed object.
* @throws ParserException If an error occurs parsing the data.
* @throws IOException If an error occurs reading data from the stream.
*/
protected abstract T parse(String connectionUrl, InputStream inputStream, String inputEncoding)
throws ParserException, IOException;
private URLConnection configureConnection(URL url) throws IOException {
URLConnection connection = url.openConnection();
connection.setConnectTimeout(timeoutMillis);
connection.setReadTimeout(timeoutMillis);
connection.setDoOutput(false);
connection.setRequestProperty("User-Agent", userAgent);
connection.connect();
return connection;
}
}