diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java index 82e91c52dfb..da294190f78 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionFetcher.java @@ -18,6 +18,8 @@ import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.util.ManifestFetcher; +import android.net.Uri; + import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; @@ -57,9 +59,9 @@ public MediaPresentationDescriptionFetcher( @Override protected MediaPresentationDescription parse(InputStream stream, String inputEncoding, - String contentId) throws IOException, ParserException { + String contentId, Uri baseUrl) throws IOException, ParserException { try { - return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId); + return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl); } catch (XmlPullParserException e) { throw new ParserException(e); } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java index d14fd05f09e..18a00f99ac7 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java @@ -64,14 +64,15 @@ public MediaPresentationDescriptionParser() { * @param inputStream The stream from which to parse the manifest. * @param inputEncoding The encoding of the input. * @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. * @throws IOException If a problem occurred reading from the stream. * @throws XmlPullParserException If a problem occurred parsing the stream as xml. * @throws ParserException If a problem occurred parsing the xml as a DASH mpd. */ public MediaPresentationDescription parseMediaPresentationDescription(InputStream inputStream, - String inputEncoding, String contentId) throws XmlPullParserException, IOException, - ParserException { + String inputEncoding, String contentId, Uri baseUrl) throws XmlPullParserException, + IOException, ParserException { XmlPullParser xpp = xmlParserFactory.newPullParser(); xpp.setInput(inputStream, inputEncoding); int eventType = xpp.next(); @@ -79,11 +80,12 @@ public MediaPresentationDescription parseMediaPresentationDescription(InputStrea throw new ParserException( "inputStream does not contain a valid media presentation description"); } - return parseMediaPresentationDescription(xpp, contentId); + return parseMediaPresentationDescription(xpp, contentId, baseUrl); } 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 minBufferTime = parseDurationMs(xpp, "minBufferTime"); String typeString = xpp.getAttributeValue(null, "type"); @@ -93,8 +95,10 @@ private MediaPresentationDescription parseMediaPresentationDescription(XmlPullPa List periods = new ArrayList(); do { xpp.next(); - if (isStartTag(xpp, "Period")) { - periods.add(parsePeriod(xpp, contentId, duration)); + if (isStartTag(xpp, "BaseURL")) { + baseUrl = parseBaseUrl(xpp, parentBaseUrl); + } else if (isStartTag(xpp, "Period")) { + periods.add(parsePeriod(xpp, contentId, baseUrl, duration)); } } while (!isEndTag(xpp, "MPD")); @@ -102,8 +106,9 @@ private MediaPresentationDescription parseMediaPresentationDescription(XmlPullPa periods); } - private Period parsePeriod(XmlPullParser xpp, String contentId, long mediaPresentationDuration) - throws XmlPullParserException, IOException { + private Period parsePeriod(XmlPullParser xpp, String contentId, Uri parentBaseUrl, + long mediaPresentationDuration) throws XmlPullParserException, IOException { + Uri baseUrl = parentBaseUrl; int id = parseInt(xpp, "id"); long start = parseDurationMs(xpp, "start", 0); long duration = parseDurationMs(xpp, "duration", mediaPresentationDuration); @@ -115,8 +120,10 @@ private Period parsePeriod(XmlPullParser xpp, String contentId, long mediaPresen long presentationTimeOffset = 0; do { xpp.next(); - if (isStartTag(xpp, "AdaptationSet")) { - adaptationSets.add(parseAdaptationSet(xpp, contentId, start, duration, + if (isStartTag(xpp, "BaseURL")) { + baseUrl = parseBaseUrl(xpp, parentBaseUrl); + } else if (isStartTag(xpp, "AdaptationSet")) { + adaptationSets.add(parseAdaptationSet(xpp, contentId, baseUrl, start, duration, segmentTimelineList)); } else if (isStartTag(xpp, "SegmentList")) { segmentStartNumber = parseInt(xpp, "startNumber"); @@ -151,9 +158,10 @@ private List parsePeriodSegmentList( return segmentTimelineList; } - private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, long periodStart, - long periodDuration, List segmentTimelineList) + private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, Uri parentBaseUrl, + long periodStart, long periodDuration, List segmentTimelineList) throws XmlPullParserException, IOException { + Uri baseUrl = parentBaseUrl; int id = -1; int contentType = AdaptationSet.TYPE_UNKNOWN; @@ -175,7 +183,9 @@ private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, lo do { xpp.next(); 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) { contentProtections = new ArrayList(); } @@ -187,8 +197,8 @@ private AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, lo : "audio".equals(contentTypeString) ? AdaptationSet.TYPE_AUDIO : AdaptationSet.TYPE_UNKNOWN; } else if (isStartTag(xpp, "Representation")) { - representations.add(parseRepresentation(xpp, contentId, periodStart, periodDuration, - mimeType, segmentTimelineList)); + representations.add(parseRepresentation(xpp, contentId, baseUrl, periodStart, + periodDuration, mimeType, segmentTimelineList)); } } } while (!isEndTag(xpp, "AdaptationSet")); @@ -208,9 +218,10 @@ protected ContentProtection parseContentProtection(XmlPullParser xpp) return new ContentProtection(schemeUriId, null); } - private Representation parseRepresentation(XmlPullParser xpp, String contentId, long periodStart, - long periodDuration, String parentMimeType, List segmentTimelineList) - throws XmlPullParserException, IOException { + private Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri parentBaseUrl, + long periodStart, long periodDuration, String parentMimeType, + List segmentTimelineList) throws XmlPullParserException, IOException { + Uri baseUrl = parentBaseUrl; String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth") / 8; int audioSamplingRate = parseInt(xpp, "audioSamplingRate"); @@ -222,7 +233,6 @@ private Representation parseRepresentation(XmlPullParser xpp, String contentId, mimeType = parentMimeType; } - String representationUrl = null; long indexStart = -1; long indexEnd = -1; long initializationStart = -1; @@ -232,8 +242,7 @@ private Representation parseRepresentation(XmlPullParser xpp, String contentId, do { xpp.next(); if (isStartTag(xpp, "BaseURL")) { - xpp.next(); - representationUrl = xpp.getText(); + baseUrl = parseBaseUrl(xpp, parentBaseUrl); } else if (isStartTag(xpp, "AudioChannelConfiguration")) { numChannels = Integer.parseInt(xpp.getAttributeValue(null, "value")); } else if (isStartTag(xpp, "SegmentBase")) { @@ -249,15 +258,14 @@ private Representation parseRepresentation(XmlPullParser xpp, String contentId, } } while (!isEndTag(xpp, "Representation")); - Uri uri = Uri.parse(representationUrl); Format format = new Format(id, mimeType, width, height, numChannels, audioSamplingRate, bandwidth); 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, periodDuration); } else { - return new SegmentedRepresentation(contentId, format, uri, initializationStart, + return new SegmentedRepresentation(contentId, format, baseUrl, initializationStart, initializationEnd, indexStart, indexEnd, periodStart, periodDuration, segmentList); } } @@ -321,7 +329,7 @@ private long parseDurationMs(XmlPullParser xpp, String name) { 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); if (value != null) { Matcher matcher = DURATION.matcher(value); @@ -340,4 +348,16 @@ private long parseDurationMs(XmlPullParser xpp, String name, long 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(); + } + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java index 192020f3e9f..f19a054346a 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestFetcher.java @@ -18,6 +18,8 @@ import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.util.ManifestFetcher; +import android.net.Uri; + import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; @@ -56,7 +58,7 @@ public SmoothStreamingManifestFetcher(ManifestCallback @Override protected SmoothStreamingManifest parse(InputStream stream, String inputEncoding, - String contentId) throws IOException, ParserException { + String contentId, Uri baseUrl) throws IOException, ParserException { try { return parser.parse(stream, inputEncoding); } catch (XmlPullParserException e) { diff --git a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java index bcf19890afa..5e362f4671e 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ManifestFetcher.java @@ -17,6 +17,7 @@ import com.google.android.exoplayer.ParserException; +import android.net.Uri; import android.os.AsyncTask; import java.io.IOException; @@ -84,14 +85,15 @@ public ManifestFetcher(ManifestCallback callback, int timeoutMillis) { protected final T doInBackground(String... data) { try { contentId = data.length > 1 ? data[1] : null; - URL url = new URL(data[0]); + String urlString = data[0]; String inputEncoding = null; InputStream inputStream = null; try { - HttpURLConnection connection = configureHttpConnection(url); + Uri baseUrl = Util.parseBaseUri(urlString); + HttpURLConnection connection = configureHttpConnection(new URL(urlString)); inputStream = connection.getInputStream(); inputEncoding = connection.getContentEncoding(); - return parse(inputStream, inputEncoding, contentId); + return parse(inputStream, inputEncoding, contentId, baseUrl); } finally { if (inputStream != null) { inputStream.close(); @@ -119,11 +121,13 @@ protected final void onPostExecute(T manifest) { * @param stream The input stream to read. * @param inputEncoding The encoding of the input stream. * @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 ParserException If an error occurred parsing the loaded data. */ - protected abstract T parse(InputStream stream, String inputEncoding, String contentId) throws - IOException, ParserException; + protected abstract T parse(InputStream stream, String inputEncoding, String contentId, + Uri baseUrl) throws IOException, ParserException; private HttpURLConnection configureHttpConnection(URL url) throws IOException { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index ee328ba8b0d..5ebd400133d 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -17,6 +17,8 @@ import com.google.android.exoplayer.upstream.DataSource; +import android.net.Uri; + import java.io.IOException; import java.net.URL; import java.util.Arrays; @@ -115,6 +117,17 @@ public static String toLowerInvariant(String text) { 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) * a specified key.