Add support for relative baseUrls in DASH manifests.

Ref: Issue #2
This commit is contained in:
Oliver Woodman 2014-07-09 23:15:58 +01:00
parent 1b957268a6
commit 9e16dec2f8
5 changed files with 74 additions and 33 deletions

View File

@ -18,6 +18,8 @@ package com.google.android.exoplayer.dash.mpd;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import android.net.Uri;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException; import java.io.IOException;
@ -57,9 +59,9 @@ public final class MediaPresentationDescriptionFetcher extends
@Override @Override
protected MediaPresentationDescription parse(InputStream stream, String inputEncoding, protected MediaPresentationDescription parse(InputStream stream, String inputEncoding,
String contentId) throws IOException, ParserException { String contentId, Uri baseUrl) throws IOException, ParserException {
try { try {
return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId); return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl);
} catch (XmlPullParserException e) { } catch (XmlPullParserException e) {
throw new ParserException(e); throw new ParserException(e);
} }

View File

@ -64,14 +64,15 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
* @param inputStream The stream from which to parse the manifest. * @param inputStream The stream from which to parse the manifest.
* @param inputEncoding The encoding of the input. * @param inputEncoding The encoding of the input.
* @param contentId The content id of the media. * @param contentId The content id of the media.
* @param baseUrl The url that any relative urls defined within the manifest are relative to.
* @return The parsed manifest. * @return The parsed manifest.
* @throws IOException If a problem occurred reading from the stream. * @throws IOException If a problem occurred reading from the stream.
* @throws XmlPullParserException If a problem occurred parsing the stream as xml. * @throws XmlPullParserException If a problem occurred parsing the stream as xml.
* @throws ParserException If a problem occurred parsing the xml as a DASH mpd. * @throws ParserException If a problem occurred parsing the xml as a DASH mpd.
*/ */
public MediaPresentationDescription parseMediaPresentationDescription(InputStream inputStream, public MediaPresentationDescription parseMediaPresentationDescription(InputStream inputStream,
String inputEncoding, String contentId) throws XmlPullParserException, IOException, String inputEncoding, String contentId, Uri baseUrl) throws XmlPullParserException,
ParserException { IOException, ParserException {
XmlPullParser xpp = xmlParserFactory.newPullParser(); XmlPullParser xpp = xmlParserFactory.newPullParser();
xpp.setInput(inputStream, inputEncoding); xpp.setInput(inputStream, inputEncoding);
int eventType = xpp.next(); int eventType = xpp.next();
@ -79,11 +80,12 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
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); return parseMediaPresentationDescription(xpp, contentId, baseUrl);
} }
private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp, private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
String contentId) throws XmlPullParserException, IOException { String contentId, Uri parentBaseUrl) throws XmlPullParserException, IOException {
Uri baseUrl = parentBaseUrl;
long duration = parseDurationMs(xpp, "mediaPresentationDuration"); long duration = parseDurationMs(xpp, "mediaPresentationDuration");
long minBufferTime = parseDurationMs(xpp, "minBufferTime"); long minBufferTime = parseDurationMs(xpp, "minBufferTime");
String typeString = xpp.getAttributeValue(null, "type"); String typeString = xpp.getAttributeValue(null, "type");
@ -93,8 +95,10 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
List<Period> periods = new ArrayList<Period>(); List<Period> periods = new ArrayList<Period>();
do { do {
xpp.next(); xpp.next();
if (isStartTag(xpp, "Period")) { if (isStartTag(xpp, "BaseURL")) {
periods.add(parsePeriod(xpp, contentId, duration)); baseUrl = parseBaseUrl(xpp, parentBaseUrl);
} else if (isStartTag(xpp, "Period")) {
periods.add(parsePeriod(xpp, contentId, baseUrl, duration));
} }
} while (!isEndTag(xpp, "MPD")); } while (!isEndTag(xpp, "MPD"));
@ -102,8 +106,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
periods); periods);
} }
private Period parsePeriod(XmlPullParser xpp, String contentId, long mediaPresentationDuration) private Period parsePeriod(XmlPullParser xpp, String contentId, Uri parentBaseUrl,
throws XmlPullParserException, IOException { long mediaPresentationDuration) throws XmlPullParserException, IOException {
Uri baseUrl = parentBaseUrl;
int id = parseInt(xpp, "id"); int id = parseInt(xpp, "id");
long start = parseDurationMs(xpp, "start", 0); long start = parseDurationMs(xpp, "start", 0);
long duration = parseDurationMs(xpp, "duration", mediaPresentationDuration); long duration = parseDurationMs(xpp, "duration", mediaPresentationDuration);
@ -115,8 +120,10 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
long presentationTimeOffset = 0; long presentationTimeOffset = 0;
do { do {
xpp.next(); xpp.next();
if (isStartTag(xpp, "AdaptationSet")) { if (isStartTag(xpp, "BaseURL")) {
adaptationSets.add(parseAdaptationSet(xpp, contentId, start, duration, baseUrl = parseBaseUrl(xpp, parentBaseUrl);
} else if (isStartTag(xpp, "AdaptationSet")) {
adaptationSets.add(parseAdaptationSet(xpp, contentId, baseUrl, start, duration,
segmentTimelineList)); segmentTimelineList));
} else if (isStartTag(xpp, "SegmentList")) { } else if (isStartTag(xpp, "SegmentList")) {
segmentStartNumber = parseInt(xpp, "startNumber"); segmentStartNumber = parseInt(xpp, "startNumber");
@ -151,9 +158,10 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
return segmentTimelineList; return segmentTimelineList;
} }
private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, long periodStart, private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, Uri parentBaseUrl,
long periodDuration, List<Segment.Timeline> segmentTimelineList) long periodStart, long periodDuration, List<Segment.Timeline> segmentTimelineList)
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
Uri baseUrl = parentBaseUrl;
int id = -1; int id = -1;
int contentType = AdaptationSet.TYPE_UNKNOWN; int contentType = AdaptationSet.TYPE_UNKNOWN;
@ -175,7 +183,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
do { do {
xpp.next(); xpp.next();
if (contentType != AdaptationSet.TYPE_UNKNOWN) { if (contentType != AdaptationSet.TYPE_UNKNOWN) {
if (isStartTag(xpp, "ContentProtection")) { if (isStartTag(xpp, "BaseURL")) {
baseUrl = parseBaseUrl(xpp, parentBaseUrl);
} else if (isStartTag(xpp, "ContentProtection")) {
if (contentProtections == null) { if (contentProtections == null) {
contentProtections = new ArrayList<ContentProtection>(); contentProtections = new ArrayList<ContentProtection>();
} }
@ -187,8 +197,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
: "audio".equals(contentTypeString) ? AdaptationSet.TYPE_AUDIO : "audio".equals(contentTypeString) ? AdaptationSet.TYPE_AUDIO
: AdaptationSet.TYPE_UNKNOWN; : AdaptationSet.TYPE_UNKNOWN;
} else if (isStartTag(xpp, "Representation")) { } else if (isStartTag(xpp, "Representation")) {
representations.add(parseRepresentation(xpp, contentId, periodStart, periodDuration, representations.add(parseRepresentation(xpp, contentId, baseUrl, periodStart,
mimeType, segmentTimelineList)); periodDuration, mimeType, segmentTimelineList));
} }
} }
} while (!isEndTag(xpp, "AdaptationSet")); } while (!isEndTag(xpp, "AdaptationSet"));
@ -208,9 +218,10 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
return new ContentProtection(schemeUriId, null); return new ContentProtection(schemeUriId, null);
} }
private Representation parseRepresentation(XmlPullParser xpp, String contentId, long periodStart, private Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri parentBaseUrl,
long periodDuration, String parentMimeType, List<Segment.Timeline> segmentTimelineList) long periodStart, long periodDuration, String parentMimeType,
throws XmlPullParserException, IOException { List<Segment.Timeline> segmentTimelineList) throws XmlPullParserException, IOException {
Uri baseUrl = parentBaseUrl;
String id = xpp.getAttributeValue(null, "id"); String id = xpp.getAttributeValue(null, "id");
int bandwidth = parseInt(xpp, "bandwidth") / 8; int bandwidth = parseInt(xpp, "bandwidth") / 8;
int audioSamplingRate = parseInt(xpp, "audioSamplingRate"); int audioSamplingRate = parseInt(xpp, "audioSamplingRate");
@ -222,7 +233,6 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
mimeType = parentMimeType; mimeType = parentMimeType;
} }
String representationUrl = null;
long indexStart = -1; long indexStart = -1;
long indexEnd = -1; long indexEnd = -1;
long initializationStart = -1; long initializationStart = -1;
@ -232,8 +242,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
do { do {
xpp.next(); xpp.next();
if (isStartTag(xpp, "BaseURL")) { if (isStartTag(xpp, "BaseURL")) {
xpp.next(); baseUrl = parseBaseUrl(xpp, parentBaseUrl);
representationUrl = xpp.getText();
} else if (isStartTag(xpp, "AudioChannelConfiguration")) { } else if (isStartTag(xpp, "AudioChannelConfiguration")) {
numChannels = Integer.parseInt(xpp.getAttributeValue(null, "value")); numChannels = Integer.parseInt(xpp.getAttributeValue(null, "value"));
} else if (isStartTag(xpp, "SegmentBase")) { } else if (isStartTag(xpp, "SegmentBase")) {
@ -249,15 +258,14 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
} }
} while (!isEndTag(xpp, "Representation")); } while (!isEndTag(xpp, "Representation"));
Uri uri = Uri.parse(representationUrl);
Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate, Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate,
bandwidth); bandwidth);
if (segmentList == null) { if (segmentList == null) {
return new Representation(contentId, -1, format, uri, DataSpec.LENGTH_UNBOUNDED, return new Representation(contentId, -1, format, baseUrl, DataSpec.LENGTH_UNBOUNDED,
initializationStart, initializationEnd, indexStart, indexEnd, periodStart, initializationStart, initializationEnd, indexStart, indexEnd, periodStart,
periodDuration); periodDuration);
} else { } else {
return new SegmentedRepresentation(contentId, format, uri, initializationStart, return new SegmentedRepresentation(contentId, format, baseUrl, initializationStart,
initializationEnd, indexStart, indexEnd, periodStart, periodDuration, segmentList); initializationEnd, indexStart, indexEnd, periodStart, periodDuration, segmentList);
} }
} }
@ -321,7 +329,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
return parseDurationMs(xpp, name, -1); return parseDurationMs(xpp, name, -1);
} }
private long parseDurationMs(XmlPullParser xpp, String name, long defaultValue) { private static long parseDurationMs(XmlPullParser xpp, String name, long defaultValue) {
String value = xpp.getAttributeValue(null, name); String value = xpp.getAttributeValue(null, name);
if (value != null) { if (value != null) {
Matcher matcher = DURATION.matcher(value); Matcher matcher = DURATION.matcher(value);
@ -340,4 +348,16 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
return defaultValue; return defaultValue;
} }
private static Uri parseBaseUrl(XmlPullParser xpp, Uri parentBaseUrl)
throws XmlPullParserException, IOException {
xpp.next();
String newBaseUrlText = xpp.getText();
Uri newBaseUri = Uri.parse(newBaseUrlText);
if (newBaseUri.isAbsolute()) {
return newBaseUri;
} else {
return parentBaseUrl.buildUpon().appendEncodedPath(newBaseUrlText).build();
}
}
} }

View File

@ -18,6 +18,8 @@ package com.google.android.exoplayer.smoothstreaming;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import android.net.Uri;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException; import java.io.IOException;
@ -56,7 +58,7 @@ public final class SmoothStreamingManifestFetcher extends ManifestFetcher<Smooth
@Override @Override
protected SmoothStreamingManifest parse(InputStream stream, String inputEncoding, protected SmoothStreamingManifest parse(InputStream stream, String inputEncoding,
String contentId) throws IOException, ParserException { String contentId, Uri baseUrl) throws IOException, ParserException {
try { try {
return parser.parse(stream, inputEncoding); return parser.parse(stream, inputEncoding);
} catch (XmlPullParserException e) { } catch (XmlPullParserException e) {

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer.util;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import java.io.IOException; import java.io.IOException;
@ -84,14 +85,15 @@ public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> {
protected final T doInBackground(String... data) { protected final T doInBackground(String... data) {
try { try {
contentId = data.length > 1 ? data[1] : null; contentId = data.length > 1 ? data[1] : null;
URL url = new URL(data[0]); String urlString = data[0];
String inputEncoding = null; String inputEncoding = null;
InputStream inputStream = null; InputStream inputStream = null;
try { try {
HttpURLConnection connection = configureHttpConnection(url); Uri baseUrl = Util.parseBaseUri(urlString);
HttpURLConnection connection = configureHttpConnection(new URL(urlString));
inputStream = connection.getInputStream(); inputStream = connection.getInputStream();
inputEncoding = connection.getContentEncoding(); inputEncoding = connection.getContentEncoding();
return parse(inputStream, inputEncoding, contentId); return parse(inputStream, inputEncoding, contentId, baseUrl);
} finally { } finally {
if (inputStream != null) { if (inputStream != null) {
inputStream.close(); inputStream.close();
@ -119,11 +121,13 @@ public abstract class ManifestFetcher<T> extends AsyncTask<String, Void, T> {
* @param stream The input stream to read. * @param stream The input stream to read.
* @param inputEncoding The encoding of the input stream. * @param inputEncoding The encoding of the input stream.
* @param contentId The content id of the media. * @param contentId The content id of the media.
* @param baseUrl Required where the manifest contains urls that are relative to a base url. May
* be null where this is not the case.
* @throws IOException If an error occurred loading the data. * @throws IOException If an error occurred loading the data.
* @throws ParserException If an error occurred parsing the loaded data. * @throws ParserException If an error occurred parsing the loaded data.
*/ */
protected abstract T parse(InputStream stream, String inputEncoding, String contentId) throws protected abstract T parse(InputStream stream, String inputEncoding, String contentId,
IOException, ParserException; Uri baseUrl) throws IOException, ParserException;
private HttpURLConnection configureHttpConnection(URL url) throws IOException { private HttpURLConnection configureHttpConnection(URL url) throws IOException {
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); HttpURLConnection connection = (HttpURLConnection) url.openConnection();

View File

@ -17,6 +17,8 @@ package com.google.android.exoplayer.util;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import android.net.Uri;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.Arrays; import java.util.Arrays;
@ -115,6 +117,17 @@ public final class Util {
return text == null ? null : text.toLowerCase(Locale.US); return text == null ? null : text.toLowerCase(Locale.US);
} }
/**
* Like {@link Uri#parse(String)}, but discards the part of the uri that follows the final
* forward slash.
*
* @param uriString An RFC 2396-compliant, encoded uri.
* @return The parsed base uri.
*/
public static Uri parseBaseUri(String uriString) {
return Uri.parse(uriString.substring(0, uriString.lastIndexOf('/')));
}
/** /**
* Returns the index of the largest value in an array that is less than (or optionally equal to) * Returns the index of the largest value in an array that is less than (or optionally equal to)
* a specified key. * a specified key.