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);
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);
}

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

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.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer.upstream.UriDataSource;
import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
@ -45,15 +46,13 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
private final String userAgent;
private final String url;
private final String contentId;
private DemoPlayer player;
private RendererBuilderCallback callback;
public HlsRendererBuilder(String userAgent, String url, String contentId) {
public HlsRendererBuilder(String userAgent, String url) {
this.userAgent = userAgent;
this.url = url;
this.contentId = contentId;
}
@Override
@ -62,17 +61,17 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
this.callback = callback;
HlsPlaylistParser parser = new HlsPlaylistParser();
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);
}
@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);

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

View File

@ -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;
}

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

View File

@ -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;
}
}

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -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

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

View File

@ -43,6 +43,19 @@ public class DataSourceInputStream extends InputStream {
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
public int read() throws IOException {
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;
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<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 InvalidContentTypeException} is thrown from
* {@link #open(DataSpec)}.
* When the source is open, returns the url from which data is being read.
* <p>
* 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<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 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;
}
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<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;
}
void clearAllRequestProperties();
}

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
* {@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));
}
/**

View File

@ -15,9 +15,10 @@
*/
package com.google.android.exoplayer.util;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.upstream.HttpDataSource;
import com.google.android.exoplayer.upstream.Loader;
import com.google.android.exoplayer.upstream.Loader.Loadable;
import com.google.android.exoplayer.upstream.NetworkLoadable;
import android.os.Handler;
import android.os.Looper;
@ -25,11 +26,22 @@ import android.os.SystemClock;
import android.util.Pair;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.CancellationException;
/**
* Performs both single and repeated loads of media manifests.
* <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.
*/
@ -58,24 +70,21 @@ public class ManifestFetcher<T> 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<T> parser;
/* package */ final String contentId;
/* package */ final String userAgent;
private final NetworkLoadable.Parser<T> parser;
private final HttpDataSource httpDataSource;
private final Handler eventHandler;
private final EventListener eventListener;
@ -83,7 +92,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
private int enabledCount;
private Loader loader;
private ManifestLoadable<T> currentLoadable;
private NetworkLoadable<T> currentLoadable;
private int loadExceptionCount;
private long loadExceptionTimestamp;
@ -92,23 +101,29 @@ public class ManifestFetcher<T> implements Loader.Callback {
private volatile T manifest;
private volatile long manifestLoadTimestamp;
public ManifestFetcher(ManifestParser<T> parser, String contentId, String manifestUrl,
String userAgent) {
this(parser, contentId, manifestUrl, userAgent, null, null);
/**
* @param manifestUrl The manifest location.
* @param httpDataSource The {@link HttpDataSource} to use when loading the manifest.
* @param parser A parser to parse the loaded manifest data.
*/
public ManifestFetcher(String manifestUrl, HttpDataSource httpDataSource,
NetworkLoadable.Parser<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 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,
String userAgent, Handler eventHandler, EventListener eventListener) {
public ManifestFetcher(String manifestUrl, HttpDataSource httpDataSource,
NetworkLoadable.Parser<T> parser, Handler eventHandler, EventListener eventListener) {
this.parser = parser;
this.contentId = contentId;
this.manifestUrl = manifestUrl;
this.userAgent = userAgent;
this.httpDataSource = httpDataSource;
this.eventHandler = eventHandler;
this.eventListener = eventListener;
}
@ -130,7 +145,8 @@ public class ManifestFetcher<T> implements Loader.Callback {
* @param callback The callback to receive the result.
*/
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();
}
@ -203,7 +219,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
loader = new Loader("manifestLoader");
}
if (!loader.isLoading()) {
currentLoadable = new ManifestLoadable<T>(manifestUrl, userAgent, contentId, parser);
currentLoadable = new NetworkLoadable<T>(manifestUrl, httpDataSource, parser);
loader.startLoading(currentLoadable, this);
notifyManifestRefreshStarted();
}
@ -287,16 +303,17 @@ public class ManifestFetcher<T> implements Loader.Callback {
private class SingleFetchHelper implements Loader.Callback {
private final NetworkLoadable<T> singleUseLoadable;
private final Looper callbackLooper;
private final ManifestCallback<T> wrappedCallback;
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.wrappedCallback = wrappedCallback;
singleUseLoader = new Loader("manifestLoader:single");
singleUseLoadable = new ManifestLoadable<T>(manifestUrl, userAgent, contentId, parser);
}
public void startLoading() {
@ -308,7 +325,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
try {
T result = singleUseLoadable.getResult();
onSingleFetchCompleted(result);
wrappedCallback.onManifest(contentId, result);
wrappedCallback.onSingleManifest(result);
} finally {
releaseLoader();
}
@ -319,7 +336,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
// This shouldn't ever happen, but handle it anyway.
try {
IOException exception = new IOException("Load cancelled", new CancellationException());
wrappedCallback.onManifestError(contentId, exception);
wrappedCallback.onSingleManifestError(exception);
} finally {
releaseLoader();
}
@ -328,7 +345,7 @@ public class ManifestFetcher<T> implements Loader.Callback {
@Override
public void onLoadError(Loadable loadable, IOException exception) {
try {
wrappedCallback.onManifestError(contentId, exception);
wrappedCallback.onSingleManifestError(exception);
} finally {
releaseLoader();
}
@ -340,24 +357,4 @@ public class ManifestFetcher<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;
}
}