From 8c424798c44cebf1a5feafa95de7d4fd34e7c1aa Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 1 Nov 2017 08:49:57 -0700 Subject: [PATCH] Fill manifest drm info with media files' pssh when needed ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174185407 --- .../com/google/android/exoplayer2/Format.java | 43 +++++++++- .../drm/DefaultDrmSessionManager.java | 17 ++-- .../android/exoplayer2/drm/DrmInitData.java | 41 +++++++-- .../exoplayer2/drm/DrmSessionManager.java | 4 +- .../dash/manifest/DashManifestParser.java | 86 ++++++++++++------- 5 files changed, 144 insertions(+), 47 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index ba68d6de33..4bd23e2cb6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -21,6 +21,7 @@ import android.media.MediaFormat; import android.os.Parcel; import android.os.Parcelable; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -464,8 +465,8 @@ public final class Format implements Parcelable { float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate; @C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags; String language = this.language == null ? manifestFormat.language : this.language; - DrmInitData drmInitData = manifestFormat.drmInitData != null ? manifestFormat.drmInitData - : this.drmInitData; + DrmInitData drmInitData = manifestFormat.drmInitData != null + ? getFilledManifestDrmData(manifestFormat.drmInitData) : this.drmInitData; return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, @@ -731,4 +732,42 @@ public final class Format implements Parcelable { }; + private DrmInitData getFilledManifestDrmData(DrmInitData manifestDrmData) { + // All exposed SchemeDatas must include key request information. + ArrayList exposedSchemeDatas = new ArrayList<>(); + ArrayList emptySchemeDatas = new ArrayList<>(); + for (int i = 0; i < manifestDrmData.schemeDataCount; i++) { + SchemeData schemeData = manifestDrmData.get(i); + if (schemeData.hasData()) { + exposedSchemeDatas.add(schemeData); + } else /* needs initialization data filling */ { + emptySchemeDatas.add(schemeData); + } + } + + if (emptySchemeDatas.isEmpty()) { + // Manifest DRM information is complete. + return manifestDrmData; + } else if (drmInitData == null) { + // The manifest DRM data needs filling but this format does not include enough information to + // do it. A subset of the manifest's scheme datas should not be exposed because a + // DrmSessionManager could decide it does not support the format, while the missing + // information comes in a format feed immediately after. + return null; + } + + int needFillingCount = emptySchemeDatas.size(); + for (int i = 0; i < drmInitData.schemeDataCount; i++) { + SchemeData mediaSchemeData = drmInitData.get(i); + for (int j = 0; j < needFillingCount; j++) { + if (mediaSchemeData.canReplace(emptySchemeDatas.get(j))) { + exposedSchemeDatas.add(mediaSchemeData); + break; + } + } + } + return exposedSchemeDatas.isEmpty() ? null : new DrmInitData(manifestDrmData.schemeType, + exposedSchemeDatas.toArray(new SchemeData[exposedSchemeDatas.size()])); + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 2ec5040aef..badacca9ac 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -340,7 +340,7 @@ public class DefaultDrmSessionManager implements DrmSe @Override public boolean canAcquireSession(@NonNull DrmInitData drmInitData) { - SchemeData schemeData = getSchemeData(drmInitData, uuid); + SchemeData schemeData = getSchemeData(drmInitData, uuid, true); if (schemeData == null) { // No data for this manager's scheme. return false; @@ -371,7 +371,7 @@ public class DefaultDrmSessionManager implements DrmSe byte[] initData = null; String mimeType = null; if (offlineLicenseKeySetId == null) { - SchemeData data = getSchemeData(drmInitData, uuid); + SchemeData data = getSchemeData(drmInitData, uuid, false); if (data == null) { final IllegalStateException error = new IllegalStateException( "Media does not support uuid: " + uuid); @@ -467,15 +467,19 @@ public class DefaultDrmSessionManager implements DrmSe * * @param drmInitData The {@link DrmInitData} from which to extract the {@link SchemeData}. * @param uuid The UUID. + * @param allowMissingData Whether a {@link SchemeData} with null {@link SchemeData#data} may be + * returned. * @return The extracted {@link SchemeData}, or null if no suitable data is present. */ - private static SchemeData getSchemeData(DrmInitData drmInitData, UUID uuid) { + private static SchemeData getSchemeData(DrmInitData drmInitData, UUID uuid, + boolean allowMissingData) { // Look for matching scheme data (matching the Common PSSH box for ClearKey). List matchingSchemeDatas = new ArrayList<>(drmInitData.schemeDataCount); for (int i = 0; i < drmInitData.schemeDataCount; i++) { SchemeData schemeData = drmInitData.get(i); - if (schemeData.matches(uuid) - || (C.CLEARKEY_UUID.equals(uuid) && schemeData.matches(C.COMMON_PSSH_UUID))) { + boolean uuidMatches = schemeData.matches(uuid) + || (C.CLEARKEY_UUID.equals(uuid) && schemeData.matches(C.COMMON_PSSH_UUID)); + if (uuidMatches && (schemeData.data != null || allowMissingData)) { matchingSchemeDatas.add(schemeData); } } @@ -488,7 +492,8 @@ public class DefaultDrmSessionManager implements DrmSe if (C.WIDEVINE_UUID.equals(uuid)) { for (int i = 0; i < matchingSchemeDatas.size(); i++) { SchemeData matchingSchemeData = matchingSchemeDatas.get(i); - int version = PsshAtomUtil.parseVersion(matchingSchemeData.data); + int version = matchingSchemeData.hasData() + ? PsshAtomUtil.parseVersion(matchingSchemeData.data) : -1; if (Util.SDK_INT < 23 && version == 0) { return matchingSchemeData; } else if (Util.SDK_INT >= 23 && version == 1) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index 703efcb452..73b443dcec 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -54,6 +54,14 @@ public final class DrmInitData implements Comparator, Parcelable { this(null, false, schemeDatas.toArray(new SchemeData[schemeDatas.size()])); } + /** + * @param schemeType See {@link #schemeType}. + * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. + */ + public DrmInitData(String schemeType, List schemeDatas) { + this(schemeType, false, schemeDatas.toArray(new SchemeData[schemeDatas.size()])); + } + /** * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. */ @@ -62,7 +70,7 @@ public final class DrmInitData implements Comparator, Parcelable { } /** - * @param schemeType The protection scheme type, or null if not applicable or unknown. + * @param schemeType See {@link #schemeType}. * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. */ public DrmInitData(@Nullable String schemeType, SchemeData... schemeDatas) { @@ -203,7 +211,7 @@ public final class DrmInitData implements Comparator, Parcelable { */ public final String mimeType; /** - * The initialization data. + * The initialization data. May be null for scheme support checks only. */ public final byte[] data; /** @@ -214,8 +222,8 @@ public final class DrmInitData implements Comparator, Parcelable { /** * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is * universal (i.e. applies to all schemes). - * @param mimeType The mimeType of the initialization data. - * @param data The initialization data. + * @param mimeType See {@link #mimeType}. + * @param data See {@link #data}. */ public SchemeData(UUID uuid, String mimeType, byte[] data) { this(uuid, mimeType, data, false); @@ -224,14 +232,14 @@ public final class DrmInitData implements Comparator, Parcelable { /** * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is * universal (i.e. applies to all schemes). - * @param mimeType The mimeType of the initialization data. - * @param data The initialization data. - * @param requiresSecureDecryption Whether secure decryption is required. + * @param mimeType See {@link #mimeType}. + * @param data See {@link #data}. + * @param requiresSecureDecryption See {@link #requiresSecureDecryption}. */ public SchemeData(UUID uuid, String mimeType, byte[] data, boolean requiresSecureDecryption) { this.uuid = Assertions.checkNotNull(uuid); this.mimeType = Assertions.checkNotNull(mimeType); - this.data = Assertions.checkNotNull(data); + this.data = data; this.requiresSecureDecryption = requiresSecureDecryption; } @@ -252,6 +260,23 @@ public final class DrmInitData implements Comparator, Parcelable { return C.UUID_NIL.equals(uuid) || schemeUuid.equals(uuid); } + /** + * Returns whether this {@link SchemeData} can be used to replace {@code other}. + * + * @param other A {@link SchemeData}. + * @return Whether this {@link SchemeData} can be used to replace {@code other}. + */ + public boolean canReplace(SchemeData other) { + return hasData() && !other.hasData() && matches(other.uuid); + } + + /** + * Returns whether {@link #data} is non-null. + */ + public boolean hasData() { + return data != null; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof SchemeData)) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java index e4b7059860..cf3d97d0b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.drm; import android.annotation.TargetApi; import android.os.Looper; +import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; /** * Manages a DRM session. @@ -39,7 +40,8 @@ public interface DrmSessionManager { * must be returned to {@link #releaseSession(DrmSession)} when it is no longer required. * * @param playbackLooper The looper associated with the media playback thread. - * @param drmInitData DRM initialization data. + * @param drmInitData DRM initialization data. All contained {@link SchemeData}s must contain + * non-null {@link SchemeData#data}. * @return The DRM session. */ DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData); 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 410fd7e41e..0c35ef0d10 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 @@ -346,45 +346,54 @@ public class DashManifestParser extends DefaultHandler protected Pair parseContentProtection(XmlPullParser xpp) throws XmlPullParserException, IOException { String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); - boolean isPlayReady = "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95".equals(schemeIdUri); String schemeType = null; byte[] data = null; UUID uuid = null; boolean requiresSecureDecoder = false; - if ("urn:mpeg:dash:mp4protection:2011".equals(schemeIdUri)) { - schemeType = xpp.getAttributeValue(null, "value"); - String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); - if (defaultKid != null && !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) { - UUID keyId = UUID.fromString(defaultKid); - data = PsshAtomUtil.buildPsshAtom(C.COMMON_PSSH_UUID, new UUID[] {keyId}, null); - uuid = C.COMMON_PSSH_UUID; - } + switch (schemeIdUri) { + case "urn:mpeg:dash:mp4protection:2011": + schemeType = xpp.getAttributeValue(null, "value"); + String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); + if (defaultKid != null && !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) { + UUID keyId = UUID.fromString(defaultKid); + data = PsshAtomUtil.buildPsshAtom(C.COMMON_PSSH_UUID, new UUID[] {keyId}, null); + uuid = C.COMMON_PSSH_UUID; + } + break; + case "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95": + uuid = C.PLAYREADY_UUID; + break; + case "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed": + uuid = C.WIDEVINE_UUID; + break; + default: + break; } do { xpp.next(); - if (data == null && XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") - && xpp.next() == XmlPullParser.TEXT) { - // The cenc:pssh element is defined in 23001-7:2015. - data = Base64.decode(xpp.getText(), Base64.DEFAULT); - uuid = PsshAtomUtil.parseUuid(data); - if (uuid == null) { - Log.w(TAG, "Skipping malformed cenc:pssh data"); - data = null; - } - } else if (data == null && isPlayReady && XmlPullParserUtil.isStartTag(xpp, "mspr:pro") - && xpp.next() == XmlPullParser.TEXT) { - // The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady. - data = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, - Base64.decode(xpp.getText(), Base64.DEFAULT)); - uuid = C.PLAYREADY_UUID; - } else if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) { + if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) { String robustnessLevel = xpp.getAttributeValue(null, "robustness_level"); requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); + } else if (data == null) { + if (XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) { + // The cenc:pssh element is defined in 23001-7:2015. + data = Base64.decode(xpp.getText(), Base64.DEFAULT); + uuid = PsshAtomUtil.parseUuid(data); + if (uuid == null) { + Log.w(TAG, "Skipping malformed cenc:pssh data"); + data = null; + } + } else if (uuid == C.PLAYREADY_UUID && XmlPullParserUtil.isStartTag(xpp, "mspr:pro") + && xpp.next() == XmlPullParser.TEXT) { + // The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady. + data = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, + Base64.decode(xpp.getText(), Base64.DEFAULT)); + } } } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); - SchemeData schemeData = data != null + SchemeData schemeData = uuid != null ? new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) : null; return Pair.create(schemeType, schemeData); } @@ -518,10 +527,8 @@ public class DashManifestParser extends DefaultHandler ArrayList drmSchemeDatas = representationInfo.drmSchemeDatas; drmSchemeDatas.addAll(extraDrmSchemeDatas); if (!drmSchemeDatas.isEmpty()) { - DrmInitData drmInitData = new DrmInitData(drmSchemeDatas); - if (drmSchemeType != null) { - drmInitData = drmInitData.copyWithSchemeType(drmSchemeType); - } + filterRedundantIncompleteSchemeDatas(drmSchemeDatas); + DrmInitData drmInitData = new DrmInitData(drmSchemeType, drmSchemeDatas); format = format.copyWithDrmInitData(drmInitData); } ArrayList inbandEventStreams = representationInfo.inbandEventStreams; @@ -728,6 +735,25 @@ public class DashManifestParser extends DefaultHandler // Utility methods. + /** + * Removes unnecessary {@link SchemeData}s with null {@link SchemeData#data}. + */ + private static void filterRedundantIncompleteSchemeDatas(ArrayList schemeDatas) { + for (int i = schemeDatas.size() - 1; i >= 0; i--) { + SchemeData schemeData = schemeDatas.get(i); + if (!schemeData.hasData()) { + for (int j = 0; j < schemeDatas.size(); j++) { + if (schemeDatas.get(j).canReplace(schemeData)) { + // schemeData is incomplete, but there is another matching SchemeData which does contain + // data, so we remove the incomplete one. + schemeDatas.remove(i); + break; + } + } + } + } + } + /** * Derives a sample mimeType from a container mimeType and codecs attribute. *