From b8e719b101e529922485ff8afc3ad00c1401f7c9 Mon Sep 17 00:00:00 2001 From: Justin Yorke Date: Fri, 6 Oct 2017 14:52:29 -0700 Subject: [PATCH] Allow multiple PSSH boxes for same system. Updates DefaultDrmSessionManager to use the prefered Widevine version (v1 on >= 23 and v0 for < 23). For other DRM schemes, uses the first scheme found. --- .../drm/DefaultDrmSessionManager.java | 35 +++++++++++--- .../android/exoplayer2/drm/DrmInitData.java | 13 +++--- .../extractor/mp4/PsshAtomUtil.java | 46 +++++++++++++++---- .../exoplayer2/drm/DrmInitDataTest.java | 45 ++++++++++-------- 4 files changed, 98 insertions(+), 41 deletions(-) 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 a0d5a932f2..9125efa458 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 @@ -323,7 +323,7 @@ public class DefaultDrmSessionManager implements DrmSe // If there is no scheme information, assume patternless AES-CTR. return true; } else if (C.CENC_TYPE_cbc1.equals(schemeType) || C.CENC_TYPE_cbcs.equals(schemeType) - || C.CENC_TYPE_cens.equals(schemeType)) { + || C.CENC_TYPE_cens.equals(schemeType)) { // AES-CBC and pattern encryption are supported on API 24 onwards. return Util.SDK_INT >= 24; } @@ -331,6 +331,7 @@ public class DefaultDrmSessionManager implements DrmSe return true; } + @Override public DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData) { Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper); @@ -435,12 +436,34 @@ public class DefaultDrmSessionManager implements DrmSe * @return The extracted {@link SchemeData}, or null if no suitable data is present. */ private static SchemeData getSchemeData(DrmInitData drmInitData, UUID uuid) { - SchemeData schemeData = drmInitData.get(uuid); - if (schemeData == null && C.CLEARKEY_UUID.equals(uuid)) { - // If present, the Common PSSH box should be used for ClearKey. - schemeData = drmInitData.get(C.COMMON_PSSH_UUID); + List schemeDatas = new ArrayList<>(); + // Look for matching PSSH boxes, or the common box in the case of ClearKey + 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))) { + schemeDatas.add(schemeData); + } } - return schemeData; + + if (schemeDatas.isEmpty()) { + return null; + } + + // For Widevine, we prefer v1 init data on M and higher, v0 for lower + if (C.WIDEVINE_UUID.equals(uuid)) { + for (SchemeData schemeData : schemeDatas ) { + int version = PsshAtomUtil.parseVersion(schemeData.data); + if (Util.SDK_INT < 23 && version == 0) { + return schemeData; + } else if (Util.SDK_INT >= 23 && version == 1) { + return schemeData; + } + } + } + + // If we don't have any special handling for this system, we take the first scheme data found + return schemeDatas.get(0); } private static byte[] getSchemeInitData(SchemeData data, UUID uuid) { 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 e346ab800f..e9aeb78f2d 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 @@ -22,6 +22,8 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; + +import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; @@ -78,12 +80,7 @@ public final class DrmInitData implements Comparator, Parcelable { // Sorting ensures that universal scheme data (i.e. data that applies to all schemes) is matched // last. It's also required by the equals and hashcode implementations. Arrays.sort(schemeDatas, this); - // Check for no duplicates. - for (int i = 1; i < schemeDatas.length; i++) { - if (schemeDatas[i - 1].uuid.equals(schemeDatas[i].uuid)) { - throw new IllegalArgumentException("Duplicate data for uuid: " + schemeDatas[i].uuid); - } - } + this.schemeDatas = schemeDatas; schemeDataCount = schemeDatas.length; } @@ -97,9 +94,11 @@ public final class DrmInitData implements Comparator, Parcelable { /** * Retrieves data for a given DRM scheme, specified by its UUID. * + * @deprecated This will only get the first data found for the scheme. * @param uuid The DRM scheme's UUID. * @return The initialization data for the scheme, or null if the scheme is not supported. */ + @Deprecated public SchemeData get(UUID uuid) { for (SchemeData schemeData : schemeDatas) { if (schemeData.matches(uuid)) { @@ -112,7 +111,7 @@ public final class DrmInitData implements Comparator, Parcelable { /** * Retrieves the {@link SchemeData} at a given index. * - * @param index index of the scheme to return. + * @param index index of the scheme to return. Must not exceed {@link #schemeDataCount}. * @return The {@link SchemeData} at the index. */ public SchemeData get(int index) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java index cfca015348..9854c57414 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java @@ -86,11 +86,28 @@ public final class PsshAtomUtil { * an unsupported version. */ public static UUID parseUuid(byte[] atom) { - Pair parsedAtom = parsePsshAtom(atom); + PsshAtom parsedAtom = parsePsshAtom(atom); if (parsedAtom == null) { return null; } - return parsedAtom.first; + return parsedAtom.uuid; + } + + /** + * Parses the version from a PSSH atom. Version 0 and 1 PSSH atoms are supported. + *

+ * The UUID is only parsed if the data is a valid PSSH atom. + * + * @param atom The atom to parse. + * @return The parsed UUID. -1 if the input is not a valid PSSH atom, or if the PSSH atom has + * an unsupported version. + */ + public static int parseVersion(byte[] atom) { + PsshAtom parsedAtom = parsePsshAtom(atom); + if (parsedAtom == null) { + return -1; + } + return parsedAtom.version; } /** @@ -105,15 +122,15 @@ public final class PsshAtomUtil { * PSSH atom has an unsupported version, or if the PSSH atom does not match the passed UUID. */ public static byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) { - Pair parsedAtom = parsePsshAtom(atom); + PsshAtom parsedAtom = parsePsshAtom(atom); if (parsedAtom == null) { return null; } - if (uuid != null && !uuid.equals(parsedAtom.first)) { - Log.w(TAG, "UUID mismatch. Expected: " + uuid + ", got: " + parsedAtom.first + "."); + if (uuid != null && !uuid.equals(parsedAtom.uuid)) { + Log.w(TAG, "UUID mismatch. Expected: " + uuid + ", got: " + parsedAtom.uuid + "."); return null; } - return parsedAtom.second; + return parsedAtom.data; } /** @@ -125,7 +142,7 @@ public final class PsshAtomUtil { * not a valid PSSH atom, or if the PSSH atom has an unsupported version. */ // TODO: Support parsing of the key ids for version 1 PSSH atoms. - private static Pair parsePsshAtom(byte[] atom) { + private static PsshAtom parsePsshAtom(byte[] atom) { ParsableByteArray atomData = new ParsableByteArray(atom); if (atomData.limit() < Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */) { // Data too short. @@ -159,7 +176,18 @@ public final class PsshAtomUtil { } byte[] data = new byte[dataSize]; atomData.readBytes(data, 0, dataSize); - return Pair.create(uuid, data); + return new PsshAtom(uuid, atomVersion, data); } -} + private static class PsshAtom { + final UUID uuid; + final int version; + final byte[] data; + + PsshAtom(final UUID uuid, final int version, final byte[] data) { + this.uuid = uuid; + this.version = version; + this.data = data; + } + } +} \ No newline at end of file diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java index b6c068c218..610dd5368e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java @@ -31,6 +31,9 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import java.util.ArrayList; +import java.util.List; + /** * Unit test for {@link DrmInitData}. */ @@ -97,6 +100,7 @@ public class DrmInitDataTest { } @Test + @Deprecated public void testGet() { // Basic matching. DrmInitData testInitData = new DrmInitData(DATA_1, DATA_2); @@ -124,27 +128,22 @@ public class DrmInitDataTest { } @Test - public void testDuplicateSchemeDataRejected() { - try { - new DrmInitData(DATA_1, DATA_1); - fail(); - } catch (IllegalArgumentException e) { - // Expected. - } + public void testGetByIndex() { + DrmInitData testInitData = new DrmInitData(DATA_1, DATA_2); + assertThat(getAllSchemeData(testInitData)).containsAllOf(DATA_1, DATA_2); + } - try { - new DrmInitData(DATA_1, DATA_1B); - fail(); - } catch (IllegalArgumentException e) { - // Expected. - } + @Test + public void testDuplicateSchemeData() { + DrmInitData testInitData = new DrmInitData(DATA_1, DATA_1); + assertThat(testInitData.schemeDataCount).isEqualTo(2); - try { - new DrmInitData(DATA_1, DATA_2, DATA_1B); - fail(); - } catch (IllegalArgumentException e) { - // Expected. - } + testInitData = new DrmInitData(DATA_1, DATA_2, DATA_1B); + assertThat(testInitData.schemeDataCount).isEqualTo(3); + assertThat(getAllSchemeData(testInitData)).containsAllOf(DATA_1, DATA_1B, DATA_2); + // Deprecated get method should return first entry + assertThat(testInitData.get(WIDEVINE_UUID)).isEqualTo(DATA_1); + assertThat(testInitData.get(PLAYREADY_UUID)).isEqualTo(DATA_2); } @Test @@ -162,4 +161,12 @@ public class DrmInitDataTest { assertThat(DATA_UNIVERSAL.matches(UUID_NIL)).isTrue(); } + private List getAllSchemeData(DrmInitData drmInitData) { + ArrayList schemeDatas = new ArrayList<>(); + for (int i = 0; i < drmInitData.schemeDataCount; i++) { + schemeDatas.add(drmInitData.get(i)); + } + return schemeDatas; + } + }