From b898dbacadf6ce28617144c2900cb1cd225e326f Mon Sep 17 00:00:00 2001 From: kamaroyl Date: Sat, 27 Jan 2024 10:13:52 -0700 Subject: [PATCH 1/2] Update Pssh Atom Util to expose internal data class, parse v1 PSSH atoms --- .../media3/extractor/mp4/PsshAtomUtil.java | 63 +++++++++++++------ .../extractor/mp4/PsshAtomUtilTest.java | 49 +++++++++++++++ 2 files changed, 92 insertions(+), 20 deletions(-) diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/PsshAtomUtil.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/PsshAtomUtil.java index 1137f7803e..833ff3c8a2 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/PsshAtomUtil.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/PsshAtomUtil.java @@ -72,10 +72,11 @@ public final class PsshAtomUtil { if (data != null && data.length != 0) { psshBox.putInt(data.length); psshBox.put(data); - } // Else the last 4 bytes are a 0 DataSize. + } else { + psshBox.putInt(0); + } return psshBox.array(); } - /** * Returns whether the data is a valid PSSH atom. * @@ -152,23 +153,29 @@ public final class PsshAtomUtil { * @return The parsed PSSH atom. Null if the input is 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. @Nullable - private static PsshAtom parsePsshAtom(byte[] atom) { + public static PsshAtom parsePsshAtom(byte[] atom) { ParsableByteArray atomData = new ParsableByteArray(atom); if (atomData.limit() < Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */) { // Data too short. return null; } atomData.setPosition(0); + int bufferLength = atomData.bytesLeft(); int atomSize = atomData.readInt(); - if (atomSize != atomData.bytesLeft() + 4) { - // Not an atom, or incorrect atom size. + + if (atomSize != bufferLength) { + Log.w( + TAG, + "Advertised atom size (" + + atomSize + + ") does not match current buffer size: " + + bufferLength); return null; } int atomType = atomData.readInt(); if (atomType != Atom.TYPE_pssh) { - // Not an atom, or incorrect atom type. + Log.w(TAG, "Atom Type is not pssh: " + atomType); return null; } int atomVersion = Atom.parseFullAtomVersion(atomData.readInt()); @@ -177,31 +184,47 @@ public final class PsshAtomUtil { return null; } UUID uuid = new UUID(atomData.readLong(), atomData.readLong()); + UUID[] keyIds = null; + int dataSize = 0; if (atomVersion == 1) { int keyIdCount = atomData.readUnsignedIntToInt(); - atomData.skipBytes(16 * keyIdCount); - } - int dataSize = atomData.readUnsignedIntToInt(); - if (dataSize != atomData.bytesLeft()) { - // Incorrect dataSize. - return null; + keyIds = new UUID[keyIdCount]; + for (int i = 0; i < keyIdCount; ++i) { + keyIds[i] = new UUID(atomData.readLong(), atomData.readLong()); + } + } else if (atomVersion == 0) { + dataSize = atomData.readUnsignedIntToInt(); + bufferLength = atomData.bytesLeft(); + if (dataSize != bufferLength) { + Log.w( + TAG, + "Atom data size (" + dataSize + ") does not match the bytes left: " + bufferLength); + return null; + } } byte[] data = new byte[dataSize]; atomData.readBytes(data, 0, dataSize); - return new PsshAtom(uuid, atomVersion, data); + return new PsshAtom(uuid, atomVersion, data, keyIds); } - // TODO: Consider exposing this and making parsePsshAtom public. - private static class PsshAtom { + /** + * A class representing the mp4 PSSH Atom as specified in the CENC standard - systemId the UUID of + * the encryption system as specified in ISO/IEC 23009-1 section 5.8.4.1 - version the version of + * the PSSH atom, should be 0 or 1 - schemaData the binary data in the atom - keyIds the optional + * set of keyIds associated with the + */ + public static class PsshAtom { - private final UUID uuid; - private final int version; - private final byte[] schemeData; + public final UUID uuid; + public final int version; + public final byte[] schemeData; + @Nullable public final UUID[] keyIds; - public PsshAtom(UUID uuid, int version, byte[] schemeData) { + public PsshAtom(UUID uuid, int version, byte[] schemeData, @Nullable UUID[] keyIds) { this.uuid = uuid; this.version = version; this.schemeData = schemeData; + this.keyIds = keyIds; } } } diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/PsshAtomUtilTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/PsshAtomUtilTest.java index 0e26cf4df1..25d36ee956 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/PsshAtomUtilTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/PsshAtomUtilTest.java @@ -50,4 +50,53 @@ public final class PsshAtomUtilTest { parsablePsshAtom.readBytes(psshSchemeData, 0, schemeData.length); assertThat(psshSchemeData).isEqualTo(schemeData); } + + @Test + public void buildVersion1Pssh() { + byte[] schemeData = new byte[] {0, 1, 2, 3, 4, 5}; + UUID[] keyIds = new UUID[2]; + keyIds[0] = UUID.fromString("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"); + keyIds[1] = UUID.fromString("dc03d7f3-334d-b858-f114-9ab759a925fb"); + byte[] psshAtom = PsshAtomUtil.buildPsshAtom(C.WIDEVINE_UUID, keyIds, schemeData); + ParsableByteArray parsablePsshAtom = new ParsableByteArray(psshAtom); + assertThat(parsablePsshAtom.readUnsignedIntToInt()).isEqualTo(psshAtom.length); + assertThat(parsablePsshAtom.readInt()).isEqualTo(TYPE_pssh); // type + int fullAtomInt = parsablePsshAtom.readInt(); // version + flags + assertThat(parseFullAtomVersion(fullAtomInt)).isEqualTo(1); + assertThat(parseFullAtomFlags(fullAtomInt)).isEqualTo(0); + UUID systemId = new UUID(parsablePsshAtom.readLong(), parsablePsshAtom.readLong()); + assertThat(systemId).isEqualTo(WIDEVINE_UUID); + assertThat(parsablePsshAtom.readUnsignedIntToInt()).isEqualTo(2); + UUID keyId0 = new UUID(parsablePsshAtom.readLong(), parsablePsshAtom.readLong()); + assertThat(keyId0).isEqualTo(keyIds[0]); + UUID keyId1 = new UUID(parsablePsshAtom.readLong(), parsablePsshAtom.readLong()); + assertThat(keyId1).isEqualTo(keyIds[1]); + assertThat(parsablePsshAtom.readUnsignedIntToInt()).isEqualTo(schemeData.length); + byte[] psshSchemeData = new byte[schemeData.length]; + parsablePsshAtom.readBytes(psshSchemeData, 0, schemeData.length); + assertThat(psshSchemeData).isEqualTo(schemeData); + } + + @Test + public void parseV1Atom() { + byte[] psshBuffer = { + 0x00, 0x00, 0x00, 0x44, 0x70, 0x73, 0x73, 0x68, // BMFF box header (68 bytes, 'pssh') + 0x01, 0x00, 0x00, 0x00, // Full box header (version = 1, flags = 0) + 0x10, 0x77, -0x11, -0x14, -0x40, -0x4e, 0x4d, 0x02, // SystemID + -0x54, -0x1d, 0x3c, 0x1e, 0x52, -0x1e, -0x05, 0x4b, 0x00, 0x00, 0x00, 0x02, // KID_count (2) + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // First KID ("0123456789012345") + 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x48, // Second KID ("ABCDEFGHIJKLMNOP") + 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x00, 0x00, 0x00, 0x00, // Size of Data (0) + }; + PsshAtomUtil.PsshAtom psshAtom = PsshAtomUtil.parsePsshAtom(psshBuffer); + assertThat(psshAtom).isNotNull(); + System.out.println("psshAtom: " + psshAtom); + assertThat(psshAtom.version).isEqualTo(1); + assertThat(psshAtom.uuid) + .isEqualTo(UUID.fromString("1077efec-c0b2-4d02-ace3-3c1e52e2fb4b")); + assertThat(psshAtom.keyIds).isNotNull(); + assertThat(psshAtom.keyIds.length).isEqualTo(2); + assertThat(psshAtom.schemeData).isEmpty(); + } } From c28c853541e797eb6df01479521190d2dd52ecd3 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 29 Jan 2024 13:30:56 +0000 Subject: [PATCH 2/2] Further adjustments to PR #1015 - Added back parsing of scheme data for version 1 as it's technically allowed by the spec. - Made constructor of PsshAtom private to only publish the data class and not the constructor. - Formatting and Javadoc adjustments - Additional tests --- .../media3/extractor/mp4/PsshAtomUtil.java | 43 ++-- .../extractor/mp4/PsshAtomUtilTest.java | 188 ++++++++++++++++-- 2 files changed, 191 insertions(+), 40 deletions(-) diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/PsshAtomUtil.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/PsshAtomUtil.java index 833ff3c8a2..f7da3449cc 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/PsshAtomUtil.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/PsshAtomUtil.java @@ -163,19 +163,15 @@ public final class PsshAtomUtil { atomData.setPosition(0); int bufferLength = atomData.bytesLeft(); int atomSize = atomData.readInt(); - if (atomSize != bufferLength) { Log.w( TAG, - "Advertised atom size (" - + atomSize - + ") does not match current buffer size: " - + bufferLength); + "Advertised atom size (" + atomSize + ") does not match buffer size: " + bufferLength); return null; } int atomType = atomData.readInt(); if (atomType != Atom.TYPE_pssh) { - Log.w(TAG, "Atom Type is not pssh: " + atomType); + Log.w(TAG, "Atom type is not pssh: " + atomType); return null; } int atomVersion = Atom.parseFullAtomVersion(atomData.readInt()); @@ -185,42 +181,41 @@ public final class PsshAtomUtil { } UUID uuid = new UUID(atomData.readLong(), atomData.readLong()); UUID[] keyIds = null; - int dataSize = 0; if (atomVersion == 1) { int keyIdCount = atomData.readUnsignedIntToInt(); keyIds = new UUID[keyIdCount]; for (int i = 0; i < keyIdCount; ++i) { keyIds[i] = new UUID(atomData.readLong(), atomData.readLong()); } - } else if (atomVersion == 0) { - dataSize = atomData.readUnsignedIntToInt(); - bufferLength = atomData.bytesLeft(); - if (dataSize != bufferLength) { - Log.w( - TAG, - "Atom data size (" + dataSize + ") does not match the bytes left: " + bufferLength); - return null; - } + } + int dataSize = atomData.readUnsignedIntToInt(); + bufferLength = atomData.bytesLeft(); + if (dataSize != bufferLength) { + Log.w( + TAG, "Atom data size (" + dataSize + ") does not match the bytes left: " + bufferLength); + return null; } byte[] data = new byte[dataSize]; atomData.readBytes(data, 0, dataSize); return new PsshAtom(uuid, atomVersion, data, keyIds); } - /** - * A class representing the mp4 PSSH Atom as specified in the CENC standard - systemId the UUID of - * the encryption system as specified in ISO/IEC 23009-1 section 5.8.4.1 - version the version of - * the PSSH atom, should be 0 or 1 - schemaData the binary data in the atom - keyIds the optional - * set of keyIds associated with the - */ - public static class PsshAtom { + /** A class representing the mp4 PSSH Atom as specified in ISO/IEC 23001-7. */ + public static final class PsshAtom { + /** The UUID of the encryption system as specified in ISO/IEC 23009-1 section 5.8.4.1. */ public final UUID uuid; + + /** The version of the PSSH atom, either 0 or 1. */ public final int version; + + /** Binary scheme data. */ public final byte[] schemeData; + + /** Array of key IDs. Always null for version 0 and non-null for version 1. */ @Nullable public final UUID[] keyIds; - public PsshAtom(UUID uuid, int version, byte[] schemeData, @Nullable UUID[] keyIds) { + /* package */ PsshAtom(UUID uuid, int version, byte[] schemeData, @Nullable UUID[] keyIds) { this.uuid = uuid; this.version = version; this.schemeData = schemeData; diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/PsshAtomUtilTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/PsshAtomUtilTest.java index 25d36ee956..e60650fa04 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/PsshAtomUtilTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/PsshAtomUtilTest.java @@ -33,10 +33,11 @@ import org.junit.runner.RunWith; public final class PsshAtomUtilTest { @Test - public void buildPsshAtom() { + public void buildPsshAtom_version0_returnsCorrectBytes() { byte[] schemeData = new byte[] {0, 1, 2, 3, 4, 5}; + byte[] psshAtom = PsshAtomUtil.buildPsshAtom(C.WIDEVINE_UUID, schemeData); - // Read the PSSH atom back and assert its content is as expected. + ParsableByteArray parsablePsshAtom = new ParsableByteArray(psshAtom); assertThat(parsablePsshAtom.readUnsignedIntToInt()).isEqualTo(psshAtom.length); // length assertThat(parsablePsshAtom.readInt()).isEqualTo(TYPE_pssh); // type @@ -52,12 +53,14 @@ public final class PsshAtomUtilTest { } @Test - public void buildVersion1Pssh() { + public void buildPsshAtom_version1_returnsCorrectBytes() { byte[] schemeData = new byte[] {0, 1, 2, 3, 4, 5}; UUID[] keyIds = new UUID[2]; keyIds[0] = UUID.fromString("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"); keyIds[1] = UUID.fromString("dc03d7f3-334d-b858-f114-9ab759a925fb"); + byte[] psshAtom = PsshAtomUtil.buildPsshAtom(C.WIDEVINE_UUID, keyIds, schemeData); + ParsableByteArray parsablePsshAtom = new ParsableByteArray(psshAtom); assertThat(parsablePsshAtom.readUnsignedIntToInt()).isEqualTo(psshAtom.length); assertThat(parsablePsshAtom.readInt()).isEqualTo(TYPE_pssh); // type @@ -78,25 +81,178 @@ public final class PsshAtomUtilTest { } @Test - public void parseV1Atom() { + public void parsePsshAtom_version0_parsesCorrectData() { byte[] psshBuffer = { - 0x00, 0x00, 0x00, 0x44, 0x70, 0x73, 0x73, 0x68, // BMFF box header (68 bytes, 'pssh') - 0x01, 0x00, 0x00, 0x00, // Full box header (version = 1, flags = 0) - 0x10, 0x77, -0x11, -0x14, -0x40, -0x4e, 0x4d, 0x02, // SystemID - -0x54, -0x1d, 0x3c, 0x1e, 0x52, -0x1e, -0x05, 0x4b, 0x00, 0x00, 0x00, 0x02, // KID_count (2) - 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // First KID ("0123456789012345") - 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, - 0x48, // Second KID ("ABCDEFGHIJKLMNOP") - 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x00, 0x00, 0x00, 0x00, // Size of Data (0) + // BMFF box header (36 bytes, 'pssh') + 0x00, + 0x00, + 0x00, + 0x24, + 0x70, + 0x73, + 0x73, + 0x68, + // Full box header (version = 0, flags = 0) + 0x00, + 0x00, + 0x00, + 0x00, + // SystemID + 0x10, + 0x77, + -0x11, + -0x14, + -0x40, + -0x4e, + 0x4d, + 0x02, + -0x54, + -0x1d, + 0x3c, + 0x1e, + 0x52, + -0x1e, + -0x05, + 0x4b, + // Size of Data (4) + 0x00, + 0x00, + 0x00, + 0x04, + // Data bytes + 0x1a, + 0x1b, + 0x1c, + 0x1d }; + PsshAtomUtil.PsshAtom psshAtom = PsshAtomUtil.parsePsshAtom(psshBuffer); + + assertThat(psshAtom).isNotNull(); + assertThat(psshAtom.version).isEqualTo(0); + assertThat(psshAtom.uuid).isEqualTo(UUID.fromString("1077efec-c0b2-4d02-ace3-3c1e52e2fb4b")); + assertThat(psshAtom.keyIds).isNull(); + assertThat(psshAtom.schemeData).isEqualTo(new byte[] {0x1a, 0x1b, 0x1c, 0x1d}); + } + + @Test + public void parsePsshAtom_version1_parsesCorrectData() { + byte[] psshBuffer = { + // BMFF box header (68 bytes, 'pssh') + 0x00, + 0x00, + 0x00, + 0x44, + 0x70, + 0x73, + 0x73, + 0x68, + // Full box header (version = 1, flags = 0) + 0x01, + 0x00, + 0x00, + 0x00, + // SystemID + 0x10, + 0x77, + -0x11, + -0x14, + -0x40, + -0x4e, + 0x4d, + 0x02, + -0x54, + -0x1d, + 0x3c, + 0x1e, + 0x52, + -0x1e, + -0x05, + 0x4b, + // KID_count (2) + 0x00, + 0x00, + 0x00, + 0x02, + // First KID ("0123456789012345") + 0x30, + 0x31, + 0x32, + 0x33, + 0x34, + 0x35, + 0x36, + 0x37, + 0x38, + 0x39, + 0x30, + 0x31, + 0x32, + 0x33, + 0x34, + 0x35, + // Second KID ("ABCDEFGHIJKLMNOP") + 0x41, + 0x42, + 0x43, + 0x44, + 0x45, + 0x46, + 0x47, + 0x48, + 0x49, + 0x4a, + 0x4b, + 0x4c, + 0x4d, + 0x4e, + 0x4f, + 0x50, + // Size of Data (0) + 0x00, + 0x00, + 0x00, + 0x00, + }; + + PsshAtomUtil.PsshAtom psshAtom = PsshAtomUtil.parsePsshAtom(psshBuffer); + assertThat(psshAtom).isNotNull(); - System.out.println("psshAtom: " + psshAtom); assertThat(psshAtom.version).isEqualTo(1); - assertThat(psshAtom.uuid) - .isEqualTo(UUID.fromString("1077efec-c0b2-4d02-ace3-3c1e52e2fb4b")); + assertThat(psshAtom.uuid).isEqualTo(UUID.fromString("1077efec-c0b2-4d02-ace3-3c1e52e2fb4b")); assertThat(psshAtom.keyIds).isNotNull(); - assertThat(psshAtom.keyIds.length).isEqualTo(2); + assertThat(psshAtom.keyIds).hasLength(2); assertThat(psshAtom.schemeData).isEmpty(); } + + @Test + public void parsePsshAtom_version0FromBuildPsshAtom_returnsEqualData() { + byte[] schemeData = new byte[] {0, 1, 2, 3, 4, 5}; + + PsshAtomUtil.PsshAtom parsedAtom = + PsshAtomUtil.parsePsshAtom(PsshAtomUtil.buildPsshAtom(C.WIDEVINE_UUID, schemeData)); + + assertThat(parsedAtom).isNotNull(); + assertThat(parsedAtom.version).isEqualTo(0); + assertThat(parsedAtom.keyIds).isNull(); + assertThat(parsedAtom.uuid).isEqualTo(C.WIDEVINE_UUID); + assertThat(parsedAtom.schemeData).isEqualTo(new byte[] {0, 1, 2, 3, 4, 5}); + } + + @Test + public void parsePsshAtom_version1FromBuildPsshAtom_returnsEqualData() { + byte[] schemeData = new byte[] {0, 1, 2, 3, 4, 5}; + UUID[] keyIds = new UUID[2]; + keyIds[0] = UUID.fromString("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"); + keyIds[1] = UUID.fromString("dc03d7f3-334d-b858-f114-9ab759a925fb"); + + PsshAtomUtil.PsshAtom parsedAtom = + PsshAtomUtil.parsePsshAtom(PsshAtomUtil.buildPsshAtom(C.WIDEVINE_UUID, keyIds, schemeData)); + + assertThat(parsedAtom).isNotNull(); + assertThat(parsedAtom.version).isEqualTo(1); + assertThat(parsedAtom.keyIds).isEqualTo(keyIds); + assertThat(parsedAtom.uuid).isEqualTo(C.WIDEVINE_UUID); + assertThat(parsedAtom.schemeData).isEqualTo(new byte[] {0, 1, 2, 3, 4, 5}); + } }