diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/BaseUrl.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/BaseUrl.java index 0126213051..1792afd905 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/BaseUrl.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/BaseUrl.java @@ -29,7 +29,7 @@ public final class BaseUrl { /** The URL. */ public final String url; /** The service location. */ - @Nullable public final String serviceLocation; + public final String serviceLocation; /** The priority. */ public final int priority; /** The weight. */ @@ -44,7 +44,7 @@ public final class BaseUrl { } /** Creates an instance. */ - public BaseUrl(String url, @Nullable String serviceLocation, int priority, int weight) { + public BaseUrl(String url, String serviceLocation, int priority, int weight) { this.url = url; this.serviceLocation = serviceLocation; this.priority = priority; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index ba47b3c5a2..3db266f6df 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -43,6 +43,7 @@ import com.google.android.exoplayer2.util.XmlPullParserUtil; import com.google.common.base.Ascii; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -99,8 +100,9 @@ public class DashManifestParser extends DefaultHandler 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"); + throw ParserException.createForMalformedManifest( + "inputStream does not contain a valid media presentation description", + /* cause= */ null); } return parseMediaPresentationDescription(xpp, new BaseUrl(uri.toString())); } catch (XmlPullParserException e) { @@ -108,8 +110,8 @@ public class DashManifestParser extends DefaultHandler } } - protected DashManifest parseMediaPresentationDescription(XmlPullParser xpp, BaseUrl baseUrl) - throws XmlPullParserException, IOException { + protected DashManifest parseMediaPresentationDescription( + XmlPullParser xpp, BaseUrl documentBaseUrl) throws XmlPullParserException, IOException { long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", C.TIME_UNSET); long durationMs = parseDuration(xpp, "mediaPresentationDuration", C.TIME_UNSET); long minBufferTimeMs = parseDuration(xpp, "minBufferTime", C.TIME_UNSET); @@ -127,8 +129,10 @@ public class DashManifestParser extends DefaultHandler Uri location = null; ServiceDescriptionElement serviceDescription = null; long baseUrlAvailabilityTimeOffsetUs = dynamic ? 0 : C.TIME_UNSET; + ArrayList parentBaseUrls = Lists.newArrayList(documentBaseUrl); List periods = new ArrayList<>(); + ArrayList baseUrls = new ArrayList<>(); long nextPeriodStartMs = dynamic ? C.TIME_UNSET : 0; boolean seenEarlyAccessPeriod = false; boolean seenFirstBaseUrl = false; @@ -138,9 +142,9 @@ public class DashManifestParser extends DefaultHandler if (!seenFirstBaseUrl) { baseUrlAvailabilityTimeOffsetUs = parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs); - baseUrl = parseBaseUrl(xpp, baseUrl); seenFirstBaseUrl = true; } + baseUrls.addAll(parseBaseUrl(xpp, parentBaseUrls)); } else if (XmlPullParserUtil.isStartTag(xpp, "ProgramInformation")) { programInformation = parseProgramInformation(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "UTCTiming")) { @@ -153,7 +157,7 @@ public class DashManifestParser extends DefaultHandler Pair periodWithDurationMs = parsePeriod( xpp, - baseUrl, + !baseUrls.isEmpty() ? baseUrls : parentBaseUrls, nextPeriodStartMs, baseUrlAvailabilityTimeOffsetUs, availabilityStartTime, @@ -271,7 +275,7 @@ public class DashManifestParser extends DefaultHandler protected Pair parsePeriod( XmlPullParser xpp, - BaseUrl baseUrl, + List parentBaseUrls, long defaultStartMs, long baseUrlAvailabilityTimeOffsetUs, long availabilityStartTimeMs, @@ -286,6 +290,7 @@ public class DashManifestParser extends DefaultHandler @Nullable Descriptor assetIdentifier = null; List adaptationSets = new ArrayList<>(); List eventStreams = new ArrayList<>(); + ArrayList baseUrls = new ArrayList<>(); boolean seenFirstBaseUrl = false; long segmentBaseAvailabilityTimeOffsetUs = C.TIME_UNSET; do { @@ -294,14 +299,14 @@ public class DashManifestParser extends DefaultHandler if (!seenFirstBaseUrl) { baseUrlAvailabilityTimeOffsetUs = parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs); - baseUrl = parseBaseUrl(xpp, baseUrl); seenFirstBaseUrl = true; } + baseUrls.addAll(parseBaseUrl(xpp, parentBaseUrls)); } else if (XmlPullParserUtil.isStartTag(xpp, "AdaptationSet")) { adaptationSets.add( parseAdaptationSet( xpp, - baseUrl, + !baseUrls.isEmpty() ? baseUrls : parentBaseUrls, segmentBase, durationMs, baseUrlAvailabilityTimeOffsetUs, @@ -361,7 +366,7 @@ public class DashManifestParser extends DefaultHandler protected AdaptationSet parseAdaptationSet( XmlPullParser xpp, - BaseUrl baseUrl, + List parentBaseUrls, @Nullable SegmentBase segmentBase, long periodDurationMs, long baseUrlAvailabilityTimeOffsetUs, @@ -389,6 +394,7 @@ public class DashManifestParser extends DefaultHandler ArrayList essentialProperties = new ArrayList<>(); ArrayList supplementalProperties = new ArrayList<>(); List representationInfos = new ArrayList<>(); + ArrayList baseUrls = new ArrayList<>(); boolean seenFirstBaseUrl = false; do { @@ -397,9 +403,9 @@ public class DashManifestParser extends DefaultHandler if (!seenFirstBaseUrl) { baseUrlAvailabilityTimeOffsetUs = parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs); - baseUrl = parseBaseUrl(xpp, baseUrl); seenFirstBaseUrl = true; } + baseUrls.addAll(parseBaseUrl(xpp, parentBaseUrls)); } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) { Pair contentProtection = parseContentProtection(xpp); if (contentProtection.first != null) { @@ -425,7 +431,7 @@ public class DashManifestParser extends DefaultHandler RepresentationInfo representationInfo = parseRepresentation( xpp, - baseUrl, + !baseUrls.isEmpty() ? baseUrls : parentBaseUrls, mimeType, codecs, width, @@ -625,7 +631,7 @@ public class DashManifestParser extends DefaultHandler protected RepresentationInfo parseRepresentation( XmlPullParser xpp, - BaseUrl baseUrl, + List parentBaseUrls, @Nullable String adaptationSetMimeType, @Nullable String adaptationSetCodecs, int adaptationSetWidth, @@ -661,6 +667,7 @@ public class DashManifestParser extends DefaultHandler ArrayList essentialProperties = new ArrayList<>(adaptationSetEssentialProperties); ArrayList supplementalProperties = new ArrayList<>(adaptationSetSupplementalProperties); + ArrayList baseUrls = new ArrayList<>(); boolean seenFirstBaseUrl = false; do { @@ -669,9 +676,9 @@ public class DashManifestParser extends DefaultHandler if (!seenFirstBaseUrl) { baseUrlAvailabilityTimeOffsetUs = parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs); - baseUrl = parseBaseUrl(xpp, baseUrl); seenFirstBaseUrl = true; } + baseUrls.addAll(parseBaseUrl(xpp, parentBaseUrls)); } else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) { audioChannels = parseAudioChannelConfiguration(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { @@ -740,7 +747,7 @@ public class DashManifestParser extends DefaultHandler return new RepresentationInfo( format, - baseUrl, + !baseUrls.isEmpty() ? baseUrls : parentBaseUrls, segmentBase, drmSchemeType, drmSchemeDatas, @@ -829,7 +836,7 @@ public class DashManifestParser extends DefaultHandler return Representation.newInstance( representationInfo.revisionId, formatBuilder.build(), - ImmutableList.of(representationInfo.baseUrl), + representationInfo.baseUrls, representationInfo.segmentBase, inbandEventStreams); } @@ -1355,12 +1362,12 @@ public class DashManifestParser extends DefaultHandler * Parses a BaseURL element. * * @param xpp The parser from which to read. - * @param parentBaseUrl A base URL for resolving the parsed URL. + * @param parentBaseUrls The parent base URLs for resolving the parsed URLs. * @throws XmlPullParserException If an error occurs parsing the element. * @throws IOException If an error occurs reading the element. - * @return The parsed and resolved URL. + * @return The list of parsed and resolved URLs. */ - protected BaseUrl parseBaseUrl(XmlPullParser xpp, BaseUrl parentBaseUrl) + protected List parseBaseUrl(XmlPullParser xpp, List parentBaseUrls) throws XmlPullParserException, IOException { @Nullable String priorityValue = xpp.getAttributeValue(null, "dvb:priority"); int priority = @@ -1372,13 +1379,21 @@ public class DashManifestParser extends DefaultHandler if (serviceLocation == null) { serviceLocation = baseUrl; } - if (!UriUtil.isAbsolute(baseUrl)) { - baseUrl = UriUtil.resolve(parentBaseUrl.url, baseUrl); + if (UriUtil.isAbsolute(baseUrl)) { + return Lists.newArrayList(new BaseUrl(baseUrl, serviceLocation, priority, weight)); + } + + List baseUrls = new ArrayList<>(); + for (int i = 0; i < parentBaseUrls.size(); i++) { + BaseUrl parentBaseUrl = parentBaseUrls.get(i); priority = parentBaseUrl.priority; weight = parentBaseUrl.weight; serviceLocation = parentBaseUrl.serviceLocation; + baseUrls.add( + new BaseUrl( + UriUtil.resolve(parentBaseUrl.url, baseUrl), serviceLocation, priority, weight)); } - return new BaseUrl(baseUrl, serviceLocation, priority, weight); + return baseUrls; } /** @@ -1882,7 +1897,7 @@ public class DashManifestParser extends DefaultHandler protected static final class RepresentationInfo { public final Format format; - public final BaseUrl baseUrl; + public final ImmutableList baseUrls; public final SegmentBase segmentBase; @Nullable public final String drmSchemeType; public final ArrayList drmSchemeDatas; @@ -1891,14 +1906,14 @@ public class DashManifestParser extends DefaultHandler public RepresentationInfo( Format format, - BaseUrl baseUrl, + List baseUrls, SegmentBase segmentBase, @Nullable String drmSchemeType, ArrayList drmSchemeDatas, ArrayList inbandEventStreams, long revisionId) { this.format = format; - this.baseUrl = baseUrl; + this.baseUrls = ImmutableList.copyOf(baseUrls); this.segmentBase = segmentBase; this.drmSchemeType = drmSchemeType; this.drmSchemeDatas = drmSchemeDatas; diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index 91198fdd95..8c3ea0c127 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableList; import java.io.IOException; import java.io.StringReader; import java.util.Collections; @@ -55,6 +56,8 @@ public class DashManifestParserTest { private static final String SAMPLE_MPD_TRICK_PLAY = "media/mpd/sample_mpd_trick_play"; private static final String SAMPLE_MPD_AVAILABILITY_TIME_OFFSET_BASE_URL = "media/mpd/sample_mpd_availabilityTimeOffset_baseUrl"; + private static final String SAMPLE_MPD_MULTIPLE_BASE_URLS = + "media/mpd/sample_mpd_multiple_baseUrls"; private static final String SAMPLE_MPD_AVAILABILITY_TIME_OFFSET_SEGMENT_TEMPLATE = "media/mpd/sample_mpd_availabilityTimeOffset_segmentTemplate"; private static final String SAMPLE_MPD_AVAILABILITY_TIME_OFFSET_SEGMENT_LIST = @@ -601,6 +604,54 @@ public class DashManifestParserTest { .isEqualTo("http://video-foo.com/baseUrl/representation3"); } + @Test + public void baseUrl_multipleBaseUrls_correctParsingAndUnfolding() throws IOException { + DashManifestParser parser = new DashManifestParser(); + DashManifest manifest = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), SAMPLE_MPD_MULTIPLE_BASE_URLS)); + + ImmutableList audioBaseUrls = + manifest.getPeriod(0).adaptationSets.get(0).representations.get(0).baseUrls; + assertThat(audioBaseUrls).hasSize(6); + assertThat(audioBaseUrls.get(0).url).endsWith("/baseUrl/a/media/audio"); + assertThat(audioBaseUrls.get(1).url).endsWith("/baseUrl/b/media/audio"); + assertThat(audioBaseUrls.get(2).url).endsWith("/baseUrl/c/media/audio"); + assertThat(audioBaseUrls.get(3).url).endsWith("/baseUrl/a/files/audio"); + assertThat(audioBaseUrls.get(4).url).endsWith("/baseUrl/b/files/audio"); + assertThat(audioBaseUrls.get(5).url).endsWith("/baseUrl/c/files/audio"); + assertThat(audioBaseUrls.get(0).serviceLocation).isEqualTo("a"); + assertThat(audioBaseUrls.get(1).serviceLocation).isEqualTo("b"); + assertThat(audioBaseUrls.get(2).serviceLocation).isEqualTo("c"); + assertThat(audioBaseUrls.get(3).serviceLocation).isEqualTo("a"); + assertThat(audioBaseUrls.get(4).serviceLocation).isEqualTo("b"); + assertThat(audioBaseUrls.get(5).serviceLocation).isEqualTo("c"); + ImmutableList videoBaseUrls = + manifest.getPeriod(0).adaptationSets.get(1).representations.get(0).baseUrls; + assertThat(videoBaseUrls).hasSize(7); + assertThat(videoBaseUrls.get(0).url).endsWith("/baseUrl/a/media/video"); + assertThat(videoBaseUrls.get(1).url).endsWith("/baseUrl/b/media/video"); + assertThat(videoBaseUrls.get(2).url).endsWith("/baseUrl/c/media/video"); + assertThat(videoBaseUrls.get(3).url).endsWith("/baseUrl/a/files/video"); + assertThat(videoBaseUrls.get(4).url).endsWith("/baseUrl/b/files/video"); + assertThat(videoBaseUrls.get(5).url).endsWith("/baseUrl/c/files/video"); + assertThat(videoBaseUrls.get(6).url).endsWith("/baseUrl/d/alternative/"); + assertThat(videoBaseUrls.get(0).serviceLocation).isEqualTo("a"); + assertThat(videoBaseUrls.get(1).serviceLocation).isEqualTo("b"); + assertThat(videoBaseUrls.get(2).serviceLocation).isEqualTo("c"); + assertThat(videoBaseUrls.get(3).serviceLocation).isEqualTo("a"); + assertThat(videoBaseUrls.get(4).serviceLocation).isEqualTo("b"); + assertThat(videoBaseUrls.get(5).serviceLocation).isEqualTo("c"); + assertThat(videoBaseUrls.get(6).serviceLocation).isEqualTo("d"); + ImmutableList textBaseUrls = + manifest.getPeriod(0).adaptationSets.get(2).representations.get(0).baseUrls; + assertThat(textBaseUrls).hasSize(1); + assertThat(textBaseUrls.get(0).url).endsWith("/baseUrl/e/text/"); + assertThat(textBaseUrls.get(0).serviceLocation).isEqualTo("e"); + } + @Test public void serviceDescriptionElement_allValuesSet() throws IOException { DashManifestParser parser = new DashManifestParser(); diff --git a/testdata/src/test/assets/media/mpd/sample_mpd_multiple_baseUrls b/testdata/src/test/assets/media/mpd/sample_mpd_multiple_baseUrls new file mode 100644 index 0000000000..228e64a03b --- /dev/null +++ b/testdata/src/test/assets/media/mpd/sample_mpd_multiple_baseUrls @@ -0,0 +1,32 @@ + + + http://video.com/baseUrl/a/ + http://video.com/baseUrl/b/ + http://video.com/baseUrl/c/ + + media/ + files/ + + audio + + + + + video + http://video.com/baseUrl/d/alternative/ + + + + + http://video.com/baseUrl/e/text/ + + + + +