From ed873322d3bc0861e5e11d25f9b9cb52730aa029 Mon Sep 17 00:00:00 2001 From: christosts Date: Tue, 13 Oct 2020 10:39:51 +0100 Subject: [PATCH] Parse ServiceDescription from DASH manifest Issue: #4904 PiperOrigin-RevId: 336838559 --- .../java/com/google/android/exoplayer2/C.java | 7 +- .../source/dash/manifest/DashManifest.java | 9 ++- .../dash/manifest/DashManifestParser.java | 28 ++++++++ .../manifest/ServiceDescriptionElement.java | 46 ++++++++++++ .../dash/manifest/DashManifestParserTest.java | 71 +++++++++++++++++++ .../dash/manifest/DashManifestTest.java | 15 +++- ...sample_mpd_service_description_low_latency | 14 ++++ ..._description_low_latency_no_playback_rates | 13 ++++ ..._description_low_latency_no_target_latency | 13 ++++ 9 files changed, 211 insertions(+), 5 deletions(-) create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/ServiceDescriptionElement.java create mode 100644 testdata/src/test/assets/media/mpd/sample_mpd_service_description_low_latency create mode 100644 testdata/src/test/assets/media/mpd/sample_mpd_service_description_low_latency_no_playback_rates create mode 100644 testdata/src/test/assets/media/mpd/sample_mpd_service_description_low_latency_no_target_latency diff --git a/library/common/src/main/java/com/google/android/exoplayer2/C.java b/library/common/src/main/java/com/google/android/exoplayer2/C.java index c4f4a2bbb5..1a1543cc7e 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/C.java @@ -60,9 +60,10 @@ public final class C { */ public static final int POSITION_UNSET = -1; - /** - * Represents an unset or unknown length. - */ + /** Represents an unset or unknown rate. */ + public static final float RATE_UNSET = -Float.MAX_VALUE; + + /** Represents an unset or unknown length. */ public static final int LENGTH_UNSET = -1; /** Represents an unset or unknown percentage. */ diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index c21af45d15..36135e0454 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -82,6 +82,9 @@ public class DashManifest implements FilterableManifest { */ @Nullable public final UtcTimingElement utcTiming; + /** The {@link ServiceDescriptionElement}, or null if not present. */ + @Nullable public final ServiceDescriptionElement serviceDescription; + /** The location of this manifest, or null if not present. */ @Nullable public final Uri location; @@ -92,7 +95,7 @@ public class DashManifest implements FilterableManifest { /** * @deprecated Use {@link #DashManifest(long, long, long, boolean, long, long, long, long, - * ProgramInformation, UtcTimingElement, Uri, List)}. + * ProgramInformation, UtcTimingElement, ServiceDescriptionElement, Uri, List)}. */ @Deprecated public DashManifest( @@ -118,6 +121,7 @@ public class DashManifest implements FilterableManifest { publishTimeMs, /* programInformation= */ null, utcTiming, + /* serviceDescription= */ null, location, periods); } @@ -133,6 +137,7 @@ public class DashManifest implements FilterableManifest { long publishTimeMs, @Nullable ProgramInformation programInformation, @Nullable UtcTimingElement utcTiming, + @Nullable ServiceDescriptionElement serviceDescription, @Nullable Uri location, List periods) { this.availabilityStartTimeMs = availabilityStartTimeMs; @@ -146,6 +151,7 @@ public class DashManifest implements FilterableManifest { this.programInformation = programInformation; this.utcTiming = utcTiming; this.location = location; + this.serviceDescription = serviceDescription; this.periods = periods == null ? Collections.emptyList() : periods; } @@ -203,6 +209,7 @@ public class DashManifest implements FilterableManifest { publishTimeMs, programInformation, utcTiming, + serviceDescription, location, copyPeriods); } 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 ec37ff064c..6f4fbf8123 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 @@ -125,6 +125,7 @@ public class DashManifestParser extends DefaultHandler ProgramInformation programInformation = null; UtcTimingElement utcTiming = null; Uri location = null; + ServiceDescriptionElement serviceDescription = null; long baseUrlAvailabilityTimeOffsetUs = dynamic ? 0 : C.TIME_UNSET; List periods = new ArrayList<>(); @@ -146,6 +147,8 @@ public class DashManifestParser extends DefaultHandler utcTiming = parseUtcTiming(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "Location")) { location = Uri.parse(xpp.nextText()); + } else if (XmlPullParserUtil.isStartTag(xpp, "ServiceDescription")) { + serviceDescription = parseServiceDescription(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "Period") && !seenEarlyAccessPeriod) { Pair periodWithDurationMs = parsePeriod( @@ -199,6 +202,7 @@ public class DashManifestParser extends DefaultHandler publishTimeMs, programInformation, utcTiming, + serviceDescription, location, periods); } @@ -214,6 +218,7 @@ public class DashManifestParser extends DefaultHandler long publishTimeMs, @Nullable ProgramInformation programInformation, @Nullable UtcTimingElement utcTiming, + @Nullable ServiceDescriptionElement serviceDescription, @Nullable Uri location, List periods) { return new DashManifest( @@ -227,6 +232,7 @@ public class DashManifestParser extends DefaultHandler publishTimeMs, programInformation, utcTiming, + serviceDescription, location, periods); } @@ -241,6 +247,23 @@ public class DashManifestParser extends DefaultHandler return new UtcTimingElement(schemeIdUri, value); } + protected ServiceDescriptionElement parseServiceDescription(XmlPullParser xpp) + throws XmlPullParserException, IOException { + long targetOffsetMs = C.TIME_UNSET; + float minPlaybackSpeed = C.RATE_UNSET; + float maxPlaybackSpeed = C.RATE_UNSET; + do { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp, "Latency")) { + targetOffsetMs = parseLong(xpp, "target", C.TIME_UNSET); + } else if (XmlPullParserUtil.isStartTag(xpp, "PlaybackRate")) { + minPlaybackSpeed = parseFloat(xpp, "min", C.RATE_UNSET); + maxPlaybackSpeed = parseFloat(xpp, "max", C.RATE_UNSET); + } + } while (!XmlPullParserUtil.isEndTag(xpp, "ServiceDescription")); + return new ServiceDescriptionElement(targetOffsetMs, minPlaybackSpeed, maxPlaybackSpeed); + } + protected Pair parsePeriod( XmlPullParser xpp, String baseUrl, @@ -1715,6 +1738,11 @@ public class DashManifestParser extends DefaultHandler return value == null ? defaultValue : Long.parseLong(value); } + protected static float parseFloat(XmlPullParser xpp, String name, float defaultValue) { + String value = xpp.getAttributeValue(null, name); + return value == null ? defaultValue : Float.parseFloat(value); + } + protected static String parseString(XmlPullParser xpp, String name, String defaultValue) { String value = xpp.getAttributeValue(null, name); return value == null ? defaultValue : value; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/ServiceDescriptionElement.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/ServiceDescriptionElement.java new file mode 100644 index 0000000000..f54f3aa2ad --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/ServiceDescriptionElement.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.dash.manifest; + +import com.google.android.exoplayer2.C; + +/** Represents a service description element. */ +public final class ServiceDescriptionElement { + + /** The target live offset in milliseconds, or {@link C#TIME_UNSET} if undefined. */ + public final long targetOffsetMs; + /** The minimum playback speed for live speed adjustment, or {@link C#RATE_UNSET} if undefined. */ + public final float minPlaybackSpeed; + /** The maximum playback speed for live speed adjustment, or {@link C#RATE_UNSET} if undefined. */ + public final float maxPlaybackSpeed; + + /** + * Creates a service description element. + * + * @param targetOffsetMs The target live offset in milliseconds, or {@link C#TIME_UNSET} if + * undefined. + * @param minPlaybackSpeed The minimum playback speed for live speed adjustment, or {@link + * C#RATE_UNSET} if undefined. + * @param maxPlaybackSpeed The maximum playback speed for live speed adjustment, or {@link + * C#RATE_UNSET} if undefined. + */ + public ServiceDescriptionElement( + long targetOffsetMs, float minPlaybackSpeed, float maxPlaybackSpeed) { + this.targetOffsetMs = targetOffsetMs; + this.minPlaybackSpeed = minPlaybackSpeed; + this.maxPlaybackSpeed = maxPlaybackSpeed; + } +} 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 496dd9575d..4eb2d37bab 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 @@ -57,6 +57,12 @@ public class DashManifestParserTest { "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 SAMPLE_MPD_SERVICE_DESCRIPTION_LOW_LATENCY = + "media/mpd/sample_mpd_service_description_low_latency"; + private static final String SAMPLE_MPD_SERVICE_DESCRIPTION_LOW_LATENCY_NO_TARGET_LATENCY = + "media/mpd/sample_mpd_service_description_low_latency_no_target_latency"; + private static final String SAMPLE_MPD_SERVICE_DESCRIPTION_LOW_LATENCY_NO_PLAYBACK_RATES = + "media/mpd/sample_mpd_service_description_low_latency_no_playback_rates"; private static final String NEXT_TAG_NAME = "Next"; private static final String NEXT_TAG = "<" + NEXT_TAG_NAME + "/>"; @@ -560,6 +566,71 @@ public class DashManifestParserTest { assertThat(getAvailabilityTimeOffsetUs(adaptationSets1.get(0))).isEqualTo(9_999_000); } + @Test + public void serviceDescriptionElement_allValuesSet() throws IOException { + DashManifestParser parser = new DashManifestParser(); + + DashManifest manifest = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), + SAMPLE_MPD_SERVICE_DESCRIPTION_LOW_LATENCY)); + + assertThat(manifest.serviceDescription).isNotNull(); + assertThat(manifest.serviceDescription.targetOffsetMs).isEqualTo(20_000); + assertThat(manifest.serviceDescription.minPlaybackSpeed).isEqualTo(0.1f); + assertThat(manifest.serviceDescription.maxPlaybackSpeed).isEqualTo(99f); + } + + @Test + public void serviceDescriptionElement_noLatency_isUnset() throws IOException { + DashManifestParser parser = new DashManifestParser(); + + DashManifest manifest = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), + SAMPLE_MPD_SERVICE_DESCRIPTION_LOW_LATENCY_NO_TARGET_LATENCY)); + + assertThat(manifest.serviceDescription).isNotNull(); + assertThat(manifest.serviceDescription.targetOffsetMs).isEqualTo(C.TIME_UNSET); + assertThat(manifest.serviceDescription.minPlaybackSpeed).isEqualTo(0.1f); + assertThat(manifest.serviceDescription.maxPlaybackSpeed).isEqualTo(99f); + } + + @Test + public void serviceDescriptionElement_noPlaybackRates_isUnset() throws IOException { + DashManifestParser parser = new DashManifestParser(); + + DashManifest manifest = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), + SAMPLE_MPD_SERVICE_DESCRIPTION_LOW_LATENCY_NO_PLAYBACK_RATES)); + + assertThat(manifest.serviceDescription).isNotNull(); + assertThat(manifest.serviceDescription.targetOffsetMs).isEqualTo(20_000); + assertThat(manifest.serviceDescription.minPlaybackSpeed).isEqualTo(C.RATE_UNSET); + assertThat(manifest.serviceDescription.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); + } + + @Test + public void serviceDescriptionElement_noServiceDescription_isNullInManifest() 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.serviceDescription).isNull(); + } + private static List buildCea608AccessibilityDescriptors(String value) { return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-608:2015", value, null)); } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java index a1b971068d..c6c5b56e7d 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.dash.manifest; import static com.google.common.truth.Truth.assertThat; import android.net.Uri; +import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.offline.StreamKey; @@ -40,9 +41,13 @@ public class DashManifestTest { @Test public void copy() { Representation[][][] representations = newRepresentations(3, 2, 3); + ServiceDescriptionElement serviceDescriptionElement = + new ServiceDescriptionElement( + /* targetOffsetMs= */ 20, /* minPlaybackSpeed= */ 0.9f, /* maxPlaybackSpeed= */ 1.1f); DashManifest sourceManifest = newDashManifest( 10, + serviceDescriptionElement, newPeriod( "1", 1, @@ -78,6 +83,7 @@ public class DashManifestTest { DashManifest expectedManifest = newDashManifest( 10, + serviceDescriptionElement, newPeriod( "1", 1, @@ -102,6 +108,7 @@ public class DashManifestTest { DashManifest sourceManifest = newDashManifest( 10, + /* serviceDescription= */ null, newPeriod("1", 1, newAdaptationSet(2, representations[0][0])), newPeriod("4", 4, newAdaptationSet(5, representations[1][0]))); @@ -111,6 +118,7 @@ public class DashManifestTest { DashManifest expectedManifest = newDashManifest( 10, + /* serviceDescription= */ null, newPeriod("1", 1, newAdaptationSet(2, representations[0][0])), newPeriod("4", 4, newAdaptationSet(5, representations[1][0]))); assertManifestEquals(expectedManifest, copyManifest); @@ -122,6 +130,7 @@ public class DashManifestTest { DashManifest sourceManifest = newDashManifest( 10, + /* serviceDescription= */ null, newPeriod( "1", 1, @@ -151,6 +160,7 @@ public class DashManifestTest { DashManifest expectedManifest = newDashManifest( 7, + /* serviceDescription= */ null, newPeriod( "1", 1, @@ -177,6 +187,7 @@ public class DashManifestTest { assertThat(actual.utcTiming).isEqualTo(expected.utcTiming); assertThat(actual.location).isEqualTo(expected.location); assertThat(actual.getPeriodCount()).isEqualTo(expected.getPeriodCount()); + assertThat(actual.serviceDescription).isEqualTo(expected.serviceDescription); for (int i = 0; i < expected.getPeriodCount(); i++) { Period expectedPeriod = expected.getPeriod(i); Period actualPeriod = actual.getPeriod(i); @@ -217,7 +228,8 @@ public class DashManifestTest { return Representation.newInstance(/* revisionId= */ 0, FORMAT, /* baseUrl= */ "", SEGMENT_BASE); } - private static DashManifest newDashManifest(int duration, Period... periods) { + private static DashManifest newDashManifest( + int duration, @Nullable ServiceDescriptionElement serviceDescription, Period... periods) { return new DashManifest( /* availabilityStartTimeMs= */ 0, duration, @@ -229,6 +241,7 @@ public class DashManifestTest { /* publishTimeMs= */ 12345, /* programInformation= */ null, UTC_TIMING, + serviceDescription, Uri.EMPTY, Arrays.asList(periods)); } diff --git a/testdata/src/test/assets/media/mpd/sample_mpd_service_description_low_latency b/testdata/src/test/assets/media/mpd/sample_mpd_service_description_low_latency new file mode 100644 index 0000000000..57b8d59ff1 --- /dev/null +++ b/testdata/src/test/assets/media/mpd/sample_mpd_service_description_low_latency @@ -0,0 +1,14 @@ + + + + + + + + + + https://test.com/0 + + + + diff --git a/testdata/src/test/assets/media/mpd/sample_mpd_service_description_low_latency_no_playback_rates b/testdata/src/test/assets/media/mpd/sample_mpd_service_description_low_latency_no_playback_rates new file mode 100644 index 0000000000..88456d4c47 --- /dev/null +++ b/testdata/src/test/assets/media/mpd/sample_mpd_service_description_low_latency_no_playback_rates @@ -0,0 +1,13 @@ + + + + + + + + + https://test.com/0 + + + + diff --git a/testdata/src/test/assets/media/mpd/sample_mpd_service_description_low_latency_no_target_latency b/testdata/src/test/assets/media/mpd/sample_mpd_service_description_low_latency_no_target_latency new file mode 100644 index 0000000000..3758459b13 --- /dev/null +++ b/testdata/src/test/assets/media/mpd/sample_mpd_service_description_low_latency_no_target_latency @@ -0,0 +1,13 @@ + + + + + + + + + https://test.com/0 + + + +