diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7c68c4b33c..2d210c85f4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,9 @@ `MetadataRenderer(MetadataOutput, Looper, MetadataDecoderFactory, boolean)` to specify whether the renderer will output metadata early or in sync with the player position. +* DASH: + * Parse ClearKey license URL from manifests + ([#10246](https://github.com/google/ExoPlayer/issues/10246)). * UI: * Ensure TalkBack announces the currently active speed option in the playback controls menu diff --git a/libraries/common/src/main/java/androidx/media3/common/DrmInitData.java b/libraries/common/src/main/java/androidx/media3/common/DrmInitData.java index 7a971ef980..65fe095828 100644 --- a/libraries/common/src/main/java/androidx/media3/common/DrmInitData.java +++ b/libraries/common/src/main/java/androidx/media3/common/DrmInitData.java @@ -18,6 +18,7 @@ package androidx.media3.common; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import androidx.annotation.CheckResult; import androidx.annotation.Nullable; import androidx.media3.common.DrmInitData.SchemeData; import androidx.media3.common.util.Assertions; @@ -157,6 +158,7 @@ public final class DrmInitData implements Comparator, Parcelable { * @param schemeType A protection scheme type. May be null. * @return A copy with the specified protection scheme type. */ + @CheckResult public DrmInitData copyWithSchemeType(@Nullable String schemeType) { if (Util.areEqual(this.schemeType, schemeType)) { return this; @@ -333,6 +335,7 @@ public final class DrmInitData implements Comparator, Parcelable { * @param data The data to include in the copy. * @return The new instance. */ + @CheckResult public SchemeData copyWithData(@Nullable byte[] data) { return new SchemeData(uuid, licenseServerUrl, mimeType, data); } diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java index 36c3695193..6c4e9bee10 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/manifest/DashManifestParser.java @@ -599,6 +599,9 @@ public class DashManifestParser extends DefaultHandler case "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed": uuid = C.WIDEVINE_UUID; break; + case "urn:uuid:e2719d58-a985-b3c9-781a-b030af78d30e": + uuid = C.CLEARKEY_UUID; + break; default: break; } @@ -606,7 +609,9 @@ public class DashManifestParser extends DefaultHandler do { xpp.next(); - if (XmlPullParserUtil.isStartTag(xpp, "ms:laurl")) { + if (XmlPullParserUtil.isStartTag(xpp, "clearkey:Laurl") && xpp.next() == XmlPullParser.TEXT) { + licenseServerUrl = xpp.getText(); + } else if (XmlPullParserUtil.isStartTag(xpp, "ms:laurl")) { licenseServerUrl = xpp.getAttributeValue(null, "licenseUrl"); } else if (data == null && XmlPullParserUtil.isStartTagIgnorePrefix(xpp, "pssh") @@ -853,6 +858,7 @@ public class DashManifestParser extends DefaultHandler ArrayList drmSchemeDatas = representationInfo.drmSchemeDatas; drmSchemeDatas.addAll(extraDrmSchemeDatas); if (!drmSchemeDatas.isEmpty()) { + fillInClearKeyInformation(drmSchemeDatas); filterRedundantIncompleteSchemeDatas(drmSchemeDatas); formatBuilder.setDrmInitData(new DrmInitData(drmSchemeType, drmSchemeDatas)); } @@ -1660,6 +1666,32 @@ public class DashManifestParser extends DefaultHandler } } + private static void fillInClearKeyInformation(ArrayList schemeDatas) { + // Find and remove ClearKey information. + @Nullable String clearKeyLicenseServerUrl = null; + for (int i = 0; i < schemeDatas.size(); i++) { + SchemeData schemeData = schemeDatas.get(i); + if (C.CLEARKEY_UUID.equals(schemeData.uuid) && schemeData.licenseServerUrl != null) { + clearKeyLicenseServerUrl = schemeData.licenseServerUrl; + schemeDatas.remove(i); + break; + } + } + if (clearKeyLicenseServerUrl == null) { + return; + } + // Fill in the ClearKey information into the existing PSSH schema data if applicable. + for (int i = 0; i < schemeDatas.size(); i++) { + SchemeData schemeData = schemeDatas.get(i); + if (C.COMMON_PSSH_UUID.equals(schemeData.uuid) && schemeData.licenseServerUrl == null) { + schemeDatas.set( + i, + new SchemeData( + C.CLEARKEY_UUID, clearKeyLicenseServerUrl, schemeData.mimeType, schemeData.data)); + } + } + } + /** * Derives a sample mimeType from a container mimeType and codecs attribute. * diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java index 667f55c197..7619322769 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/manifest/DashManifestParserTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import androidx.annotation.Nullable; import androidx.media3.common.C; +import androidx.media3.common.DrmInitData; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Util; @@ -79,6 +80,8 @@ public class DashManifestParserTest { "media/mpd/sample_mpd_service_description_low_latency_only_playback_rates"; private static final String SAMPLE_MPD_SERVICE_DESCRIPTION_LOW_LATENCY_ONLY_TARGET_LATENCY = "media/mpd/sample_mpd_service_description_low_latency_only_target_latency"; + private static final String SAMPLE_MPD_CLEAR_KEY_LICENSE_URL = + "media/mpd/sample_mpd_clear_key_license_url"; private static final String NEXT_TAG_NAME = "Next"; private static final String NEXT_TAG = "<" + NEXT_TAG_NAME + "/>"; @@ -880,6 +883,37 @@ public class DashManifestParserTest { assertThat(manifest.serviceDescription).isNull(); } + @Test + public void contentProtections_withClearKeyLicenseUrl() throws IOException { + DashManifestParser parser = new DashManifestParser(); + + DashManifest manifest = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), SAMPLE_MPD_CLEAR_KEY_LICENSE_URL)); + + assertThat(manifest.getPeriodCount()).isEqualTo(1); + Period period = manifest.getPeriod(0); + assertThat(period.adaptationSets).hasSize(2); + AdaptationSet adaptationSet0 = period.adaptationSets.get(0); + AdaptationSet adaptationSet1 = period.adaptationSets.get(1); + assertThat(adaptationSet0.representations).hasSize(1); + assertThat(adaptationSet1.representations).hasSize(1); + Representation representation0 = adaptationSet0.representations.get(0); + Representation representation1 = adaptationSet1.representations.get(0); + assertThat(representation0.format.drmInitData.schemeType).isEqualTo("cenc"); + assertThat(representation1.format.drmInitData.schemeType).isEqualTo("cenc"); + assertThat(representation0.format.drmInitData.schemeDataCount).isEqualTo(1); + assertThat(representation1.format.drmInitData.schemeDataCount).isEqualTo(1); + DrmInitData.SchemeData schemeData0 = representation0.format.drmInitData.get(0); + DrmInitData.SchemeData schemeData1 = representation1.format.drmInitData.get(0); + assertThat(schemeData0.uuid).isEqualTo(C.CLEARKEY_UUID); + assertThat(schemeData1.uuid).isEqualTo(C.CLEARKEY_UUID); + assertThat(schemeData0.licenseServerUrl).isEqualTo("https://testserver1.test/AcquireLicense"); + assertThat(schemeData1.licenseServerUrl).isEqualTo("https://testserver2.test/AcquireLicense"); + } + private static List buildCea608AccessibilityDescriptors(String value) { return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-608:2015", value, null)); } diff --git a/libraries/test_data/src/test/assets/media/mpd/sample_mpd_clear_key_license_url b/libraries/test_data/src/test/assets/media/mpd/sample_mpd_clear_key_license_url new file mode 100644 index 0000000000..ed362b729a --- /dev/null +++ b/libraries/test_data/src/test/assets/media/mpd/sample_mpd_clear_key_license_url @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + https://testserver1.test/AcquireLicense + + + + + + https://testserver2.test/AcquireLicense + + + + + +