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 e9e9c66df2..ede5df90c4 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 @@ -40,11 +40,11 @@ import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.XmlPullParserUtil; import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableList; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.regex.Matcher; @@ -115,6 +115,7 @@ public class DashManifestParser extends DefaultHandler ProgramInformation programInformation = null; UtcTimingElement utcTiming = null; Uri location = null; + long baseUrlAvailabilityTimeOffsetUs = dynamic ? 0 : C.TIME_UNSET; List periods = new ArrayList<>(); long nextPeriodStartMs = dynamic ? C.TIME_UNSET : 0; @@ -124,6 +125,8 @@ public class DashManifestParser extends DefaultHandler xpp.next(); if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) { if (!seenFirstBaseUrl) { + baseUrlAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs); baseUrl = parseBaseUrl(xpp, baseUrl); seenFirstBaseUrl = true; } @@ -134,7 +137,8 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "Location")) { location = Uri.parse(xpp.nextText()); } else if (XmlPullParserUtil.isStartTag(xpp, "Period") && !seenEarlyAccessPeriod) { - Pair periodWithDurationMs = parsePeriod(xpp, baseUrl, nextPeriodStartMs); + Pair periodWithDurationMs = + parsePeriod(xpp, baseUrl, nextPeriodStartMs, baseUrlAvailabilityTimeOffsetUs); Period period = periodWithDurationMs.first; if (period.startMs == C.TIME_UNSET) { if (dynamic) { @@ -221,7 +225,8 @@ public class DashManifestParser extends DefaultHandler return new UtcTimingElement(schemeIdUri, value); } - protected Pair parsePeriod(XmlPullParser xpp, String baseUrl, long defaultStartMs) + protected Pair parsePeriod( + XmlPullParser xpp, String baseUrl, long defaultStartMs, long baseUrlAvailabilityTimeOffsetUs) throws XmlPullParserException, IOException { @Nullable String id = xpp.getAttributeValue(null, "id"); long startMs = parseDuration(xpp, "start", defaultStartMs); @@ -231,23 +236,50 @@ public class DashManifestParser extends DefaultHandler List adaptationSets = new ArrayList<>(); List eventStreams = new ArrayList<>(); boolean seenFirstBaseUrl = false; + long segmentBaseAvailabilityTimeOffsetUs = C.TIME_UNSET; do { xpp.next(); if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) { if (!seenFirstBaseUrl) { + baseUrlAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs); baseUrl = parseBaseUrl(xpp, baseUrl); seenFirstBaseUrl = true; } } else if (XmlPullParserUtil.isStartTag(xpp, "AdaptationSet")) { - adaptationSets.add(parseAdaptationSet(xpp, baseUrl, segmentBase, durationMs)); + adaptationSets.add( + parseAdaptationSet( + xpp, + baseUrl, + segmentBase, + durationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs)); } else if (XmlPullParserUtil.isStartTag(xpp, "EventStream")) { eventStreams.add(parseEventStream(xpp)); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { - segmentBase = parseSegmentBase(xpp, null); + segmentBase = parseSegmentBase(xpp, /* parent= */ null); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { - segmentBase = parseSegmentList(xpp, null, durationMs); + segmentBaseAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, /* parentAvailabilityTimeOffsetUs= */ C.TIME_UNSET); + segmentBase = + parseSegmentList( + xpp, + /* parent= */ null, + durationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { - segmentBase = parseSegmentTemplate(xpp, null, Collections.emptyList(), durationMs); + segmentBaseAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, /* parentAvailabilityTimeOffsetUs= */ C.TIME_UNSET); + segmentBase = + parseSegmentTemplate( + xpp, + /* parent= */ null, + ImmutableList.of(), + durationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs); } else if (XmlPullParserUtil.isStartTag(xpp, "AssetIdentifier")) { assetIdentifier = parseDescriptor(xpp, "AssetIdentifier"); } else { @@ -271,7 +303,12 @@ public class DashManifestParser extends DefaultHandler // AdaptationSet parsing. protected AdaptationSet parseAdaptationSet( - XmlPullParser xpp, String baseUrl, @Nullable SegmentBase segmentBase, long periodDurationMs) + XmlPullParser xpp, + String baseUrl, + @Nullable SegmentBase segmentBase, + long periodDurationMs, + long baseUrlAvailabilityTimeOffsetUs, + long segmentBaseAvailabilityTimeOffsetUs) throws XmlPullParserException, IOException { int id = parseInt(xpp, "id", AdaptationSet.ID_UNSET); int contentType = parseContentType(xpp); @@ -299,6 +336,8 @@ public class DashManifestParser extends DefaultHandler xpp.next(); if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) { if (!seenFirstBaseUrl) { + baseUrlAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs); baseUrl = parseBaseUrl(xpp, baseUrl); seenFirstBaseUrl = true; } @@ -341,7 +380,9 @@ public class DashManifestParser extends DefaultHandler essentialProperties, supplementalProperties, segmentBase, - periodDurationMs); + periodDurationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs); contentType = checkContentTypeConsistency( contentType, MimeTypes.getTrackType(representationInfo.format.sampleMimeType)); @@ -349,11 +390,26 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { - segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase, periodDurationMs); + segmentBaseAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, segmentBaseAvailabilityTimeOffsetUs); + segmentBase = + parseSegmentList( + xpp, + (SegmentList) segmentBase, + periodDurationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { + segmentBaseAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, segmentBaseAvailabilityTimeOffsetUs); segmentBase = parseSegmentTemplate( - xpp, (SegmentTemplate) segmentBase, supplementalProperties, periodDurationMs); + xpp, + (SegmentTemplate) segmentBase, + supplementalProperties, + periodDurationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs); } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); } else if (XmlPullParserUtil.isStartTag(xpp, "Label")) { @@ -514,7 +570,9 @@ public class DashManifestParser extends DefaultHandler List adaptationSetEssentialProperties, List adaptationSetSupplementalProperties, @Nullable SegmentBase segmentBase, - long periodDurationMs) + long periodDurationMs, + long baseUrlAvailabilityTimeOffsetUs, + long segmentBaseAvailabilityTimeOffsetUs) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE); @@ -538,6 +596,8 @@ public class DashManifestParser extends DefaultHandler xpp.next(); if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) { if (!seenFirstBaseUrl) { + baseUrlAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, baseUrlAvailabilityTimeOffsetUs); baseUrl = parseBaseUrl(xpp, baseUrl); seenFirstBaseUrl = true; } @@ -546,14 +606,26 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { - segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase, periodDurationMs); + segmentBaseAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, segmentBaseAvailabilityTimeOffsetUs); + segmentBase = + parseSegmentList( + xpp, + (SegmentList) segmentBase, + periodDurationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { + segmentBaseAvailabilityTimeOffsetUs = + parseAvailabilityTimeOffsetUs(xpp, segmentBaseAvailabilityTimeOffsetUs); segmentBase = parseSegmentTemplate( xpp, (SegmentTemplate) segmentBase, adaptationSetSupplementalProperties, - periodDurationMs); + periodDurationMs, + baseUrlAvailabilityTimeOffsetUs, + segmentBaseAvailabilityTimeOffsetUs); } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) { Pair contentProtection = parseContentProtection(xpp); if (contentProtection.first != null) { @@ -718,7 +790,11 @@ public class DashManifestParser extends DefaultHandler } protected SegmentList parseSegmentList( - XmlPullParser xpp, @Nullable SegmentList parent, long periodDurationMs) + XmlPullParser xpp, + @Nullable SegmentList parent, + long periodDurationMs, + long baseUrlAvailabilityTimeOffsetUs, + long segmentBaseAvailabilityTimeOffsetUs) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -726,6 +802,9 @@ public class DashManifestParser extends DefaultHandler parent != null ? parent.presentationTimeOffset : 0); long duration = parseLong(xpp, "duration", parent != null ? parent.duration : C.TIME_UNSET); long startNumber = parseLong(xpp, "startNumber", parent != null ? parent.startNumber : 1); + long availabilityTimeOffsetUs = + getFinalAvailabilityTimeOffset( + baseUrlAvailabilityTimeOffsetUs, segmentBaseAvailabilityTimeOffsetUs); RangedUri initialization = null; List timeline = null; @@ -753,8 +832,15 @@ public class DashManifestParser extends DefaultHandler segments = segments != null ? segments : parent.mediaSegments; } - return buildSegmentList(initialization, timescale, presentationTimeOffset, - startNumber, duration, timeline, segments); + return buildSegmentList( + initialization, + timescale, + presentationTimeOffset, + startNumber, + duration, + timeline, + availabilityTimeOffsetUs, + segments); } protected SegmentList buildSegmentList( @@ -764,16 +850,26 @@ public class DashManifestParser extends DefaultHandler long startNumber, long duration, @Nullable List timeline, + long availabilityTimeOffsetUs, @Nullable List segments) { - return new SegmentList(initialization, timescale, presentationTimeOffset, - startNumber, duration, timeline, segments); + return new SegmentList( + initialization, + timescale, + presentationTimeOffset, + startNumber, + duration, + timeline, + availabilityTimeOffsetUs, + segments); } protected SegmentTemplate parseSegmentTemplate( XmlPullParser xpp, @Nullable SegmentTemplate parent, List adaptationSetSupplementalProperties, - long periodDurationMs) + long periodDurationMs, + long baseUrlAvailabilityTimeOffsetUs, + long segmentBaseAvailabilityTimeOffsetUs) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", @@ -782,6 +878,9 @@ public class DashManifestParser extends DefaultHandler long startNumber = parseLong(xpp, "startNumber", parent != null ? parent.startNumber : 1); long endNumber = parseLastSegmentNumberSupplementalProperty(adaptationSetSupplementalProperties); + long availabilityTimeOffsetUs = + getFinalAvailabilityTimeOffset( + baseUrlAvailabilityTimeOffsetUs, segmentBaseAvailabilityTimeOffsetUs); UrlTemplate mediaTemplate = parseUrlTemplate(xpp, "media", parent != null ? parent.mediaTemplate : null); @@ -815,6 +914,7 @@ public class DashManifestParser extends DefaultHandler endNumber, duration, timeline, + availabilityTimeOffsetUs, initializationTemplate, mediaTemplate); } @@ -827,6 +927,7 @@ public class DashManifestParser extends DefaultHandler long endNumber, long duration, List timeline, + long availabilityTimeOffsetUs, @Nullable UrlTemplate initializationTemplate, @Nullable UrlTemplate mediaTemplate) { return new SegmentTemplate( @@ -837,6 +938,7 @@ public class DashManifestParser extends DefaultHandler endNumber, duration, timeline, + availabilityTimeOffsetUs, initializationTemplate, mediaTemplate); } @@ -1151,6 +1253,27 @@ public class DashManifestParser extends DefaultHandler return UriUtil.resolve(parentBaseUrl, parseText(xpp, "BaseURL")); } + /** + * Parses the availabilityTimeOffset value and returns the parsed value or the parent value if it + * doesn't exist. + * + * @param xpp The parser from which to read. + * @param parentAvailabilityTimeOffsetUs The availability time offset of a parent element in + * microseconds. + * @return The parsed availabilityTimeOffset in microseconds. + */ + protected long parseAvailabilityTimeOffsetUs( + XmlPullParser xpp, long parentAvailabilityTimeOffsetUs) { + String value = xpp.getAttributeValue(/* namespace= */ null, "availabilityTimeOffset"); + if (value == null) { + return parentAvailabilityTimeOffsetUs; + } + if ("INF".equals(value)) { + return Long.MAX_VALUE; + } + return (long) (Float.parseFloat(value) * C.MICROS_PER_SECOND); + } + // AudioChannelConfiguration parsing. protected int parseAudioChannelConfiguration(XmlPullParser xpp) @@ -1569,6 +1692,20 @@ public class DashManifestParser extends DefaultHandler return C.INDEX_UNSET; } + private static long getFinalAvailabilityTimeOffset( + long baseUrlAvailabilityTimeOffsetUs, long segmentBaseAvailabilityTimeOffsetUs) { + long availabilityTimeOffsetUs = segmentBaseAvailabilityTimeOffsetUs; + if (availabilityTimeOffsetUs == C.TIME_UNSET) { + // Fall back to BaseURL values if no SegmentBase specifies an offset. + availabilityTimeOffsetUs = baseUrlAvailabilityTimeOffsetUs; + } + if (availabilityTimeOffsetUs == Long.MAX_VALUE) { + // Replace INF value with TIME_UNSET to specify that all segments are available immediately. + availabilityTimeOffsetUs = C.TIME_UNSET; + } + return availabilityTimeOffsetUs; + } + /** A parsed Representation element. */ protected static final class RepresentationInfo { diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java index 80ad15cd8f..03151631d3 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.dash.manifest; import android.net.Uri; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.dash.DashSegmentIndex; @@ -275,7 +276,7 @@ public abstract class Representation { public static class MultiSegmentRepresentation extends Representation implements DashSegmentIndex { - private final MultiSegmentBase segmentBase; + @VisibleForTesting /* package */ final MultiSegmentBase segmentBase; /** * @param revisionId Identifies the revision of the content. diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java index d6206c1c0d..5de2814b29 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java @@ -120,6 +120,15 @@ public abstract class SegmentBase { /* package */ final long duration; @Nullable /* package */ final List segmentTimeline; + /** + * Offset to the current realtime at which segments become available, in microseconds, or {@link + * C#TIME_UNSET} if all segments are available immediately. + * + *

Segments will be available once their end time ≤ currentRealTime + + * availabilityTimeOffset. + */ + /* package */ final long availabilityTimeOffsetUs; + /** * @param initialization A {@link RangedUri} corresponding to initialization data, if such data * exists. @@ -133,6 +142,8 @@ public abstract class SegmentBase { * @param segmentTimeline A segment timeline corresponding to the segments. If null, then * segments are assumed to be of fixed duration as specified by the {@code duration} * parameter. + * @param availabilityTimeOffsetUs The offset to the current realtime at which segments become + * available in microseconds, or {@link C#TIME_UNSET} if not applicable. */ public MultiSegmentBase( @Nullable RangedUri initialization, @@ -140,11 +151,13 @@ public abstract class SegmentBase { long presentationTimeOffset, long startNumber, long duration, - @Nullable List segmentTimeline) { + @Nullable List segmentTimeline, + long availabilityTimeOffsetUs) { super(initialization, timescale, presentationTimeOffset); this.startNumber = startNumber; this.duration = duration; this.segmentTimeline = segmentTimeline; + this.availabilityTimeOffsetUs = availabilityTimeOffsetUs; } /** @see DashSegmentIndex#getSegmentNum(long, long) */ @@ -255,6 +268,8 @@ public abstract class SegmentBase { * @param segmentTimeline A segment timeline corresponding to the segments. If null, then * segments are assumed to be of fixed duration as specified by the {@code duration} * parameter. + * @param availabilityTimeOffsetUs The offset to the current realtime at which segments become + * available in microseconds, or {@link C#TIME_UNSET} if not applicable. * @param mediaSegments A list of {@link RangedUri}s indicating the locations of the segments. */ public SegmentList( @@ -264,9 +279,16 @@ public abstract class SegmentBase { long startNumber, long duration, @Nullable List segmentTimeline, + long availabilityTimeOffsetUs, @Nullable List mediaSegments) { - super(initialization, timescale, presentationTimeOffset, startNumber, duration, - segmentTimeline); + super( + initialization, + timescale, + presentationTimeOffset, + startNumber, + duration, + segmentTimeline, + availabilityTimeOffsetUs); this.mediaSegments = mediaSegments; } @@ -311,6 +333,8 @@ public abstract class SegmentBase { * @param segmentTimeline A segment timeline corresponding to the segments. If null, then * segments are assumed to be of fixed duration as specified by the {@code duration} * parameter. + * @param availabilityTimeOffsetUs The offset to the current realtime at which segments become + * available in microseconds, or {@link C#TIME_UNSET} if not applicable. * @param initializationTemplate A template defining the location of initialization data, if * such data exists. If non-null then the {@code initialization} parameter is ignored. If * null then {@code initialization} will be used. @@ -324,6 +348,7 @@ public abstract class SegmentBase { long endNumber, long duration, @Nullable List segmentTimeline, + long availabilityTimeOffsetUs, @Nullable UrlTemplate initializationTemplate, @Nullable UrlTemplate mediaTemplate) { super( @@ -332,7 +357,8 @@ public abstract class SegmentBase { presentationTimeOffset, startNumber, duration, - segmentTimeline); + segmentTimeline, + availabilityTimeOffsetUs); this.initializationTemplate = initializationTemplate; this.mediaTemplate = mediaTemplate; this.endNumber = endNumber; 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 c2ea12bcd7..496dd9575d 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 @@ -51,6 +51,12 @@ public class DashManifestParserTest { private static final String SAMPLE_MPD_ASSET_IDENTIFIER = "media/mpd/sample_mpd_asset_identifier"; private static final String SAMPLE_MPD_TEXT = "media/mpd/sample_mpd_text"; 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_AVAILABILITY_TIME_OFFSET_SEGMENT_TEMPLATE = + "media/mpd/sample_mpd_availabilityTimeOffset_segmentTemplate"; + private static final String SAMPLE_MPD_AVAILABILITY_TIME_OFFSET_SEGMENT_LIST = + "media/mpd/sample_mpd_availabilityTimeOffset_segmentList"; private static final String NEXT_TAG_NAME = "Next"; private static final String NEXT_TAG = "<" + NEXT_TAG_NAME + "/>"; @@ -469,6 +475,91 @@ public class DashManifestParserTest { assertThat(assetIdentifier.id).isEqualTo("uniqueId"); } + @Test + public void availabilityTimeOffset_staticManifest_setToTimeUnset() throws IOException { + DashManifestParser parser = new DashManifestParser(); + DashManifest manifest = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), SAMPLE_MPD_TEXT)); + + assertThat(manifest.getPeriodCount()).isEqualTo(1); + List adaptationSets = manifest.getPeriod(0).adaptationSets; + assertThat(adaptationSets).hasSize(3); + assertThat(getAvailabilityTimeOffsetUs(adaptationSets.get(0))).isEqualTo(C.TIME_UNSET); + assertThat(getAvailabilityTimeOffsetUs(adaptationSets.get(1))).isEqualTo(C.TIME_UNSET); + assertThat(getAvailabilityTimeOffsetUs(adaptationSets.get(2))).isEqualTo(C.TIME_UNSET); + } + + @Test + public void availabilityTimeOffset_dynamicManifest_valuesInBaseUrl_setsCorrectValues() + throws IOException { + DashManifestParser parser = new DashManifestParser(); + DashManifest manifest = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), + SAMPLE_MPD_AVAILABILITY_TIME_OFFSET_BASE_URL)); + + assertThat(manifest.getPeriodCount()).isEqualTo(2); + List adaptationSets0 = manifest.getPeriod(0).adaptationSets; + List adaptationSets1 = manifest.getPeriod(1).adaptationSets; + assertThat(adaptationSets0).hasSize(4); + assertThat(adaptationSets1).hasSize(1); + assertThat(getAvailabilityTimeOffsetUs(adaptationSets0.get(0))).isEqualTo(5_000_000); + assertThat(getAvailabilityTimeOffsetUs(adaptationSets0.get(1))).isEqualTo(4_321_000); + assertThat(getAvailabilityTimeOffsetUs(adaptationSets0.get(2))).isEqualTo(9_876_543); + assertThat(getAvailabilityTimeOffsetUs(adaptationSets0.get(3))).isEqualTo(C.TIME_UNSET); + assertThat(getAvailabilityTimeOffsetUs(adaptationSets1.get(0))).isEqualTo(0); + } + + @Test + public void availabilityTimeOffset_dynamicManifest_valuesInSegmentTemplate_setsCorrectValues() + throws IOException { + DashManifestParser parser = new DashManifestParser(); + DashManifest manifest = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), + SAMPLE_MPD_AVAILABILITY_TIME_OFFSET_SEGMENT_TEMPLATE)); + + assertThat(manifest.getPeriodCount()).isEqualTo(2); + List adaptationSets0 = manifest.getPeriod(0).adaptationSets; + List adaptationSets1 = manifest.getPeriod(1).adaptationSets; + assertThat(adaptationSets0).hasSize(4); + assertThat(adaptationSets1).hasSize(1); + assertThat(getAvailabilityTimeOffsetUs(adaptationSets0.get(0))).isEqualTo(2_000_000); + assertThat(getAvailabilityTimeOffsetUs(adaptationSets0.get(1))).isEqualTo(3_210_000); + assertThat(getAvailabilityTimeOffsetUs(adaptationSets0.get(2))).isEqualTo(1_230_000); + assertThat(getAvailabilityTimeOffsetUs(adaptationSets0.get(3))).isEqualTo(100_000); + assertThat(getAvailabilityTimeOffsetUs(adaptationSets1.get(0))).isEqualTo(9_999_000); + } + + @Test + public void availabilityTimeOffset_dynamicManifest_valuesInSegmentList_setsCorrectValues() + throws IOException { + DashManifestParser parser = new DashManifestParser(); + DashManifest manifest = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), + SAMPLE_MPD_AVAILABILITY_TIME_OFFSET_SEGMENT_LIST)); + + assertThat(manifest.getPeriodCount()).isEqualTo(2); + List adaptationSets0 = manifest.getPeriod(0).adaptationSets; + List adaptationSets1 = manifest.getPeriod(1).adaptationSets; + assertThat(adaptationSets0).hasSize(4); + assertThat(adaptationSets1).hasSize(1); + assertThat(getAvailabilityTimeOffsetUs(adaptationSets0.get(0))).isEqualTo(2_000_000); + assertThat(getAvailabilityTimeOffsetUs(adaptationSets0.get(1))).isEqualTo(3_210_000); + assertThat(getAvailabilityTimeOffsetUs(adaptationSets0.get(2))).isEqualTo(1_230_000); + assertThat(getAvailabilityTimeOffsetUs(adaptationSets0.get(3))).isEqualTo(100_000); + assertThat(getAvailabilityTimeOffsetUs(adaptationSets1.get(0))).isEqualTo(9_999_000); + } + private static List buildCea608AccessibilityDescriptors(String value) { return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-608:2015", value, null)); } @@ -482,4 +573,13 @@ public class DashManifestParserTest { assertThat(xpp.getEventType()).isEqualTo(XmlPullParser.START_TAG); assertThat(xpp.getName()).isEqualTo(NEXT_TAG_NAME); } + + private static long getAvailabilityTimeOffsetUs(AdaptationSet adaptationSet) { + assertThat(adaptationSet.representations).isNotEmpty(); + Representation representation = adaptationSet.representations.get(0); + assertThat(representation).isInstanceOf(Representation.MultiSegmentRepresentation.class); + SegmentBase.MultiSegmentBase segmentBase = + ((Representation.MultiSegmentRepresentation) representation).segmentBase; + return segmentBase.availabilityTimeOffsetUs; + } } diff --git a/testdata/src/test/assets/media/mpd/sample_mpd_availabilityTimeOffset_baseUrl b/testdata/src/test/assets/media/mpd/sample_mpd_availabilityTimeOffset_baseUrl new file mode 100644 index 0000000000..188b2778a0 --- /dev/null +++ b/testdata/src/test/assets/media/mpd/sample_mpd_availabilityTimeOffset_baseUrl @@ -0,0 +1,40 @@ + + + + http://video.com/baseUrl + + + + + + http://video.com/baseUrl + + + + + + + http://video.com/baseUrl + + + + + http://video.com/baseUrl + + http://video.com/baseUrl + + + + + + + + + + + diff --git a/testdata/src/test/assets/media/mpd/sample_mpd_availabilityTimeOffset_segmentList b/testdata/src/test/assets/media/mpd/sample_mpd_availabilityTimeOffset_segmentList new file mode 100644 index 0000000000..364756a4aa --- /dev/null +++ b/testdata/src/test/assets/media/mpd/sample_mpd_availabilityTimeOffset_segmentList @@ -0,0 +1,46 @@ + + + http://video.com/baseUrl + + http://video.com/baseUrl + + + + http://video.com/baseUrl + + + + + http://video.com/baseUrl + + + + + + + http://video.com/baseUrl + + + + + http://video.com/baseUrl + + + http://video.com/baseUrl + + + + + + + + + + + + diff --git a/testdata/src/test/assets/media/mpd/sample_mpd_availabilityTimeOffset_segmentTemplate b/testdata/src/test/assets/media/mpd/sample_mpd_availabilityTimeOffset_segmentTemplate new file mode 100644 index 0000000000..3c1dc78ae1 --- /dev/null +++ b/testdata/src/test/assets/media/mpd/sample_mpd_availabilityTimeOffset_segmentTemplate @@ -0,0 +1,46 @@ + + + http://video.com/baseUrl + + http://video.com/baseUrl + + + + http://video.com/baseUrl + + + + + http://video.com/baseUrl + + + + + + + http://video.com/baseUrl + + + + + http://video.com/baseUrl + + + http://video.com/baseUrl + + + + + + + + + + + +