From 7e2fffe74dd7ac77246c6ebc0dd63ef3dc60b16e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 25 Nov 2015 16:47:05 +0000 Subject: [PATCH] Make sure we use the correct mimeType with DRM initialization data. We were previously using the container format of the media being played as the mimeType generating key requests, but this is not always correct. As an example, where a manifest contains webm streams but specifies initialization data using cenc:pssh elements in the manifest, the media has a webm mimeType, but the DRM initialization data has an mp4 mimeType. --- .../extractor/webm/WebmExtractorTest.java | 9 +- .../exoplayer/FrameworkSampleSource.java | 5 +- .../exoplayer/dash/DashChunkSource.java | 4 +- .../exoplayer/dash/mpd/ContentProtection.java | 22 ++--- .../MediaPresentationDescriptionParser.java | 14 +-- .../android/exoplayer/drm/DrmInitData.java | 85 +++++++++++++------ .../drm/StreamingDrmSessionManager.java | 19 ++--- .../extractor/mp4/FragmentedMp4Extractor.java | 6 +- .../extractor/webm/WebmExtractor.java | 5 +- .../SmoothStreamingChunkSource.java | 6 +- 10 files changed, 107 insertions(+), 68 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer/extractor/webm/WebmExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer/extractor/webm/WebmExtractorTest.java index e463a17e82..4011a53e9d 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/extractor/webm/WebmExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/extractor/webm/WebmExtractorTest.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.drm.DrmInitData; +import com.google.android.exoplayer.drm.DrmInitData.SchemeInitData; import com.google.android.exoplayer.extractor.ChunkIndex; import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.extractor.webm.StreamBuilder.ContentEncodingSettings; @@ -237,8 +238,12 @@ public final class WebmExtractorTest extends InstrumentationTestCase { assertIndex(DEFAULT_TIMECODE_SCALE, 1); DrmInitData drmInitData = extractorOutput.drmInitData; assertNotNull(drmInitData); - android.test.MoreAsserts.assertEquals(TEST_ENCRYPTION_KEY_ID, drmInitData.get(WIDEVINE_UUID)); - android.test.MoreAsserts.assertEquals(TEST_ENCRYPTION_KEY_ID, drmInitData.get(ZERO_UUID)); + SchemeInitData widevineInitData = drmInitData.get(WIDEVINE_UUID); + assertEquals(MimeTypes.VIDEO_WEBM, widevineInitData.mimeType); + android.test.MoreAsserts.assertEquals(TEST_ENCRYPTION_KEY_ID, widevineInitData.data); + SchemeInitData zeroInitData = drmInitData.get(ZERO_UUID); + assertEquals(MimeTypes.VIDEO_WEBM, zeroInitData.mimeType); + android.test.MoreAsserts.assertEquals(TEST_ENCRYPTION_KEY_ID, zeroInitData.data); } public void testPrepareThreeCuePoints() throws IOException, InterruptedException { diff --git a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java index 9fb4d8c216..e5ea8f7863 100644 --- a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer; import com.google.android.exoplayer.SampleSource.SampleSourceReader; import com.google.android.exoplayer.drm.DrmInitData; +import com.google.android.exoplayer.drm.DrmInitData.SchemeInitData; import com.google.android.exoplayer.extractor.ExtractorSampleSource; import com.google.android.exoplayer.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer.util.Assertions; @@ -273,10 +274,10 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe if (psshInfo == null || psshInfo.isEmpty()) { return null; } - DrmInitData.Mapped drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4); + DrmInitData.Mapped drmInitData = new DrmInitData.Mapped(); for (UUID uuid : psshInfo.keySet()) { byte[] psshAtom = PsshAtomUtil.buildPsshAtom(uuid, psshInfo.get(uuid)); - drmInitData.put(uuid, psshAtom); + drmInitData.put(uuid, new SchemeInitData(MimeTypes.VIDEO_MP4, psshAtom)); } return drmInitData; } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index f28bde2899..cd388b312a 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -1054,8 +1054,6 @@ public class DashChunkSource implements ChunkSource, Output { } private static DrmInitData getDrmInitData(AdaptationSet adaptationSet) { - String drmInitMimeType = mimeTypeIsWebm(adaptationSet.representations.get(0).format.mimeType) - ? MimeTypes.VIDEO_WEBM : MimeTypes.VIDEO_MP4; if (adaptationSet.contentProtections.isEmpty()) { return null; } else { @@ -1064,7 +1062,7 @@ public class DashChunkSource implements ChunkSource, Output { ContentProtection contentProtection = adaptationSet.contentProtections.get(i); if (contentProtection.uuid != null && contentProtection.data != null) { if (drmInitData == null) { - drmInitData = new DrmInitData.Mapped(drmInitMimeType); + drmInitData = new DrmInitData.Mapped(); } drmInitData.put(contentProtection.uuid, contentProtection.data); } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java index 8f02fdc6f4..1ebba25f4e 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java @@ -15,10 +15,10 @@ */ package com.google.android.exoplayer.dash.mpd; +import com.google.android.exoplayer.drm.DrmInitData.SchemeInitData; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Util; -import java.util.Arrays; import java.util.UUID; /** @@ -37,16 +37,16 @@ public class ContentProtection { public final UUID uuid; /** - * Protection scheme specific data. May be null. + * Protection scheme specific initialization data. May be null. */ - public final byte[] data; + public final SchemeInitData data; /** * @param schemeUriId Identifies the content protection scheme. * @param uuid The UUID of the protection scheme, if known. May be null. * @param data Protection scheme specific initialization data. May be null. */ - public ContentProtection(String schemeUriId, UUID uuid, byte[] data) { + public ContentProtection(String schemeUriId, UUID uuid, SchemeInitData data) { this.schemeUriId = Assertions.checkNotNull(schemeUriId); this.uuid = uuid; this.data = data; @@ -64,20 +64,14 @@ public class ContentProtection { ContentProtection other = (ContentProtection) obj; return schemeUriId.equals(other.schemeUriId) && Util.areEqual(uuid, other.uuid) - && Arrays.equals(data, other.data); + && Util.areEqual(data, other.data); } @Override public int hashCode() { - int hashCode = 1; - - hashCode = hashCode * 37 + schemeUriId.hashCode(); - if (uuid != null) { - hashCode = hashCode * 37 + uuid.hashCode(); - } - if (data != null) { - hashCode = hashCode * 37 + Arrays.hashCode(data); - } + int hashCode = schemeUriId.hashCode(); + hashCode = (37 * hashCode) + (uuid != null ? uuid.hashCode() : 0); + hashCode = (37 * hashCode) + (data != null ? data.hashCode() : 0); return hashCode; } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java index 19c64067c6..a205599e45 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentList; import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTemplate; import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTimelineElement; import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase; +import com.google.android.exoplayer.drm.DrmInitData.SchemeInitData; import com.google.android.exoplayer.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer.upstream.UriLoadable; import com.google.android.exoplayer.util.Assertions; @@ -317,23 +318,24 @@ public class MediaPresentationDescriptionParser extends DefaultHandler throws XmlPullParserException, IOException { String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); UUID uuid = null; - byte[] psshAtom = null; + SchemeInitData data = null; do { xpp.next(); // The cenc:pssh element is defined in 23001-7:2015 if (ParserUtil.isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) { - psshAtom = Base64.decode(xpp.getText(), Base64.DEFAULT); - uuid = PsshAtomUtil.parseUuid(psshAtom); + data = new SchemeInitData(MimeTypes.VIDEO_MP4, + Base64.decode(xpp.getText(), Base64.DEFAULT)); + uuid = PsshAtomUtil.parseUuid(data.data); if (uuid == null) { throw new ParserException("Invalid pssh atom in cenc:pssh element"); } } } while (!ParserUtil.isEndTag(xpp, "ContentProtection")); - - return buildContentProtection(schemeIdUri, uuid, psshAtom); + return buildContentProtection(schemeIdUri, uuid, data); } - protected ContentProtection buildContentProtection(String schemeIdUri, UUID uuid, byte[] data) { + protected ContentProtection buildContentProtection(String schemeIdUri, UUID uuid, + SchemeInitData data) { return new ContentProtection(schemeIdUri, uuid, data); } diff --git a/library/src/main/java/com/google/android/exoplayer/drm/DrmInitData.java b/library/src/main/java/com/google/android/exoplayer/drm/DrmInitData.java index bdb4565fc3..c01fc59ca7 100644 --- a/library/src/main/java/com/google/android/exoplayer/drm/DrmInitData.java +++ b/library/src/main/java/com/google/android/exoplayer/drm/DrmInitData.java @@ -15,25 +15,19 @@ */ package com.google.android.exoplayer.drm; +import com.google.android.exoplayer.util.Assertions; + import android.media.MediaDrm; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.UUID; /** - * Encapsulates initialization data required by a {@link MediaDrm} instance. + * Encapsulates initialization data required by a {@link MediaDrm} instances. */ -public abstract class DrmInitData { - - /** - * The container mime type. - */ - public final String mimeType; - - public DrmInitData(String mimeType) { - this.mimeType = mimeType; - } +public interface DrmInitData { /** * Retrieves initialization data for a given DRM scheme, specified by its UUID. @@ -41,22 +35,21 @@ public abstract class DrmInitData { * @param schemeUuid The DRM scheme's UUID. * @return The initialization data for the scheme, or null if the scheme is not supported. */ - public abstract byte[] get(UUID schemeUuid); + public abstract SchemeInitData get(UUID schemeUuid); /** * A {@link DrmInitData} implementation that maps UUID onto scheme specific data. */ - public static final class Mapped extends DrmInitData { + public static final class Mapped implements DrmInitData { - private final Map schemeData; + private final Map schemeData; - public Mapped(String mimeType) { - super(mimeType); + public Mapped() { schemeData = new HashMap<>(); } @Override - public byte[] get(UUID schemeUuid) { + public SchemeInitData get(UUID schemeUuid) { return schemeData.get(schemeUuid); } @@ -64,10 +57,10 @@ public abstract class DrmInitData { * Inserts scheme specific initialization data. * * @param schemeUuid The scheme UUID. - * @param data The corresponding initialization data. + * @param schemeInitData The corresponding initialization data. */ - public void put(UUID schemeUuid, byte[] data) { - schemeData.put(schemeUuid, data); + public void put(UUID schemeUuid, SchemeInitData schemeInitData) { + schemeData.put(schemeUuid, schemeInitData); } } @@ -75,20 +68,62 @@ public abstract class DrmInitData { /** * A {@link DrmInitData} implementation that returns the same initialization data for all schemes. */ - public static final class Universal extends DrmInitData { + public static final class Universal implements DrmInitData { - private byte[] data; + private SchemeInitData data; - public Universal(String mimeType, byte[] data) { - super(mimeType); + public Universal(SchemeInitData data) { this.data = data; } @Override - public byte[] get(UUID schemeUuid) { + public SchemeInitData get(UUID schemeUuid) { return data; } } + /** + * Scheme initialization data. + */ + public static final class SchemeInitData { + + /** + * The mimeType of {@link #data}. + */ + public final String mimeType; + /** + * The initialization data. + */ + public final byte[] data; + + /** + * @param mimeType The mimeType of the initialization data. + * @param data The initialization data. + */ + public SchemeInitData(String mimeType, byte[] data) { + this.mimeType = Assertions.checkNotNull(mimeType); + this.data = Assertions.checkNotNull(data); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SchemeInitData)) { + return false; + } + if (obj == this) { + return true; + } + + SchemeInitData other = (SchemeInitData) obj; + return mimeType.equals(other.mimeType) && Arrays.equals(data, other.data); + } + + @Override + public int hashCode() { + return mimeType.hashCode() + 31 * Arrays.hashCode(data); + } + + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java index a0a3ab0710..be22d4c6c8 100644 --- a/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java +++ b/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.drm; +import com.google.android.exoplayer.drm.DrmInitData.SchemeInitData; import com.google.android.exoplayer.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer.util.Util; @@ -103,8 +104,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { private int state; private MediaCrypto mediaCrypto; private Exception lastException; - private String mimeType; - private byte[] schemeData; + private SchemeInitData schemeInitData; private byte[] sessionId; /** @@ -273,20 +273,19 @@ public class StreamingDrmSessionManager implements DrmSessionManager { requestHandlerThread.start(); postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); } - if (schemeData == null) { - mimeType = drmInitData.mimeType; - schemeData = drmInitData.get(uuid); - if (schemeData == null) { + if (schemeInitData == null) { + schemeInitData = drmInitData.get(uuid); + if (schemeInitData == null) { onError(new IllegalStateException("Media does not support uuid: " + uuid)); return; } if (Util.SDK_INT < 21) { // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. - byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeData, WIDEVINE_UUID); + byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData.data, WIDEVINE_UUID); if (psshData == null) { // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. } else { - schemeData = psshData; + schemeInitData = new SchemeInitData(schemeInitData.mimeType, psshData); } } } @@ -307,7 +306,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { postRequestHandler = null; requestHandlerThread.quit(); requestHandlerThread = null; - schemeData = null; + schemeInitData = null; mediaCrypto = null; lastException = null; if (sessionId != null) { @@ -369,7 +368,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { private void postKeyRequest() { KeyRequest keyRequest; try { - keyRequest = mediaDrm.getKeyRequest(sessionId, schemeData, mimeType, + keyRequest = mediaDrm.getKeyRequest(sessionId, schemeInitData.data, schemeInitData.mimeType, MediaDrm.KEY_TYPE_STREAMING, optionalKeyRequestParameters); postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget(); } catch (NotProvisionedException e) { diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java index 8a00dd1872..56b20274e0 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/FragmentedMp4Extractor.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer.extractor.mp4; import com.google.android.exoplayer.C; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.drm.DrmInitData; +import com.google.android.exoplayer.drm.DrmInitData.SchemeInitData; import com.google.android.exoplayer.extractor.ChunkIndex; import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.ExtractorInput; @@ -292,10 +293,11 @@ public final class FragmentedMp4Extractor implements Extractor { LeafAtom child = moovChildren.get(i); if (child.type == Atom.TYPE_pssh) { if (drmInitData == null) { - drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4); + drmInitData = new DrmInitData.Mapped(); } byte[] psshData = child.data.data; - drmInitData.put(PsshAtomUtil.parseUuid(psshData), psshData); + drmInitData.put(PsshAtomUtil.parseUuid(psshData), + new SchemeInitData(MimeTypes.VIDEO_MP4, psshData)); } } if (drmInitData != null) { diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java index d805eec7ba..96ce99a638 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/webm/WebmExtractor.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.drm.DrmInitData; +import com.google.android.exoplayer.drm.DrmInitData.SchemeInitData; import com.google.android.exoplayer.extractor.ChunkIndex; import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.ExtractorInput; @@ -450,8 +451,8 @@ public final class WebmExtractor implements Extractor { throw new ParserException("Encrypted Track found but ContentEncKeyID was not found"); } if (!sentDrmInitData) { - extractorOutput.drmInitData( - new DrmInitData.Universal(MimeTypes.VIDEO_WEBM, currentTrack.encryptionKeyId)); + extractorOutput.drmInitData(new DrmInitData.Universal( + new SchemeInitData(MimeTypes.VIDEO_WEBM, currentTrack.encryptionKeyId))); sentDrmInitData = true; } } diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java index 1482f83e3e..8f83a817ac 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer.chunk.FormatEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation; import com.google.android.exoplayer.chunk.MediaChunk; import com.google.android.exoplayer.drm.DrmInitData; +import com.google.android.exoplayer.drm.DrmInitData.SchemeInitData; import com.google.android.exoplayer.extractor.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer.extractor.mp4.Track; import com.google.android.exoplayer.extractor.mp4.TrackEncryptionBox; @@ -144,8 +145,9 @@ public class SmoothStreamingChunkSource implements ChunkSource, byte[] keyId = getProtectionElementKeyId(protectionElement.data); trackEncryptionBoxes = new TrackEncryptionBox[1]; trackEncryptionBoxes[0] = new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId); - drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4); - drmInitData.put(protectionElement.uuid, protectionElement.data); + drmInitData = new DrmInitData.Mapped(); + drmInitData.put(protectionElement.uuid, + new SchemeInitData(MimeTypes.VIDEO_MP4, protectionElement.data)); } else { trackEncryptionBoxes = null; drmInitData = null;