From 1bc99c2f031de29efa3dc79e769b0e5368416220 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 15 Sep 2020 19:08:50 +0100 Subject: [PATCH] Parse availabilityTimeOffset from DASH manifest. This value is needed to figure out the last available segment for low-latency live streaming. It may be present in each BaseURL tag and each SegmentList or SegmentTemplate, with the latter one taking precedence. The value is saved as part of MultiSegmentBase where it will be used to retrieve the last available segment index in future changes. PiperOrigin-RevId: 331809871 --- .../dash/manifest/DashManifestParser.java | 177 ++++++++++++++++-- .../source/dash/manifest/Representation.java | 3 +- .../source/dash/manifest/SegmentBase.java | 34 +++- .../dash/manifest/DashManifestParserTest.java | 100 ++++++++++ .../sample_mpd_availabilityTimeOffset_baseUrl | 40 ++++ ...ple_mpd_availabilityTimeOffset_segmentList | 46 +++++ ...mpd_availabilityTimeOffset_segmentTemplate | 46 +++++ 7 files changed, 421 insertions(+), 25 deletions(-) create mode 100644 testdata/src/test/assets/media/mpd/sample_mpd_availabilityTimeOffset_baseUrl create mode 100644 testdata/src/test/assets/media/mpd/sample_mpd_availabilityTimeOffset_segmentList create mode 100644 testdata/src/test/assets/media/mpd/sample_mpd_availabilityTimeOffset_segmentTemplate 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 + + + + + + + + + + + +