From 4c4782c72dfb63fb0dbb0ca7d739c0095c16fc50 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 18 Jun 2015 14:16:37 +0100 Subject: [PATCH] Pass whole PSSH box to MediaDrm (except in the WV+L case). This fixes SmoothStreaming on AndroidTV, and also removes a warning that gets logged when using Widevine/FMP4. --- .../exoplayer/FrameworkSampleSource.java | 6 +- .../MediaPresentationDescriptionParser.java | 18 ++- .../android/exoplayer/drm/DrmInitData.java | 9 -- .../drm/StreamingDrmSessionManager.java | 24 +++- .../extractor/mp4/FragmentedMp4Extractor.java | 10 +- .../exoplayer/extractor/mp4/PsshAtomUtil.java | 122 ++++++++++++++++++ .../SmoothStreamingManifestParser.java | 3 +- 7 files changed, 157 insertions(+), 35 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/extractor/mp4/PsshAtomUtil.java 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 7d649ab80e..dbc5ccbc42 100644 --- a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer.SampleSource.SampleSourceReader; import com.google.android.exoplayer.drm.DrmInitData; import com.google.android.exoplayer.extractor.ExtractorSampleSource; import com.google.android.exoplayer.extractor.mp4.Mp4Extractor; +import com.google.android.exoplayer.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.Util; @@ -266,7 +267,10 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe return null; } DrmInitData.Mapped drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4); - drmInitData.putAll(psshInfo); + for (UUID uuid : psshInfo.keySet()) { + byte[] psshAtom = PsshAtomUtil.buildPsshAtom(uuid, psshInfo.get(uuid)); + drmInitData.put(uuid, psshAtom); + } return drmInitData; } 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 e433498735..d0af28b9cf 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,10 +21,10 @@ 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.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer.upstream.UriLoadable; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.MimeTypes; -import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.UriUtil; import com.google.android.exoplayer.util.Util; @@ -258,22 +258,20 @@ public class MediaPresentationDescriptionParser extends DefaultHandler throws XmlPullParserException, IOException { String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); UUID uuid = null; - byte[] data = null; + byte[] psshAtom = null; do { xpp.next(); // The cenc:pssh element is defined in 23001-7:2015 if (isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) { - byte[] decodedData = Base64.decode(xpp.getText(), Base64.DEFAULT); - ParsableByteArray psshAtom = new ParsableByteArray(decodedData); - psshAtom.skipBytes(12); - uuid = new UUID(psshAtom.readLong(), psshAtom.readLong()); - int dataSize = psshAtom.readInt(); - data = new byte[dataSize]; - psshAtom.readBytes(data, 0, dataSize); + psshAtom = Base64.decode(xpp.getText(), Base64.DEFAULT); + uuid = PsshAtomUtil.parseUuid(psshAtom); + if (uuid == null) { + throw new ParserException("Invalid pssh atom in cenc:pssh element"); + } } } while (!isEndTag(xpp, "ContentProtection")); - return buildContentProtection(schemeIdUri, uuid, data); + return buildContentProtection(schemeIdUri, uuid, psshAtom); } protected ContentProtection buildContentProtection(String schemeIdUri, UUID uuid, byte[] 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 7123d2d76d..bdb4565fc3 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 @@ -70,15 +70,6 @@ public abstract class DrmInitData { schemeData.put(schemeUuid, data); } - /** - * Inserts scheme specific initialization data. - * - * @param data A mapping from scheme UUID to initialization data. - */ - public void putAll(Map data) { - schemeData.putAll(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 2ee73b05e6..519a2b6987 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,9 @@ */ package com.google.android.exoplayer.drm; +import com.google.android.exoplayer.extractor.mp4.PsshAtomUtil; +import com.google.android.exoplayer.util.Util; + import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.media.DeniedByServerException; @@ -96,7 +99,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { private MediaCrypto mediaCrypto; private Exception lastException; private String mimeType; - private byte[] schemePsshData; + private byte[] schemeData; private byte[] sessionId; /** @@ -265,13 +268,22 @@ public class StreamingDrmSessionManager implements DrmSessionManager { requestHandlerThread.start(); postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); } - if (this.schemePsshData == null) { + if (schemeData == null) { mimeType = drmInitData.mimeType; - schemePsshData = drmInitData.get(uuid); - if (schemePsshData == null) { + schemeData = drmInitData.get(uuid); + if (schemeData == 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); + if (psshData == null) { + // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. + } else { + schemeData = psshData; + } + } } state = STATE_OPENING; openInternal(true); @@ -290,7 +302,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { postRequestHandler = null; requestHandlerThread.quit(); requestHandlerThread = null; - schemePsshData = null; + schemeData = null; mediaCrypto = null; lastException = null; if (sessionId != null) { @@ -352,7 +364,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager { private void postKeyRequest() { KeyRequest keyRequest; try { - keyRequest = mediaDrm.getKeyRequest(sessionId, schemePsshData, mimeType, + keyRequest = mediaDrm.getKeyRequest(sessionId, schemeData, 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 d0987decbf..6469462394 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 @@ -35,7 +35,6 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.Stack; -import java.util.UUID; /** * Facilitates the extraction of data from the fragmented mp4 container format. @@ -250,16 +249,11 @@ public final class FragmentedMp4Extractor implements Extractor { for (int i = 0; i < moovChildrenSize; i++) { LeafAtom child = moovChildren.get(i); if (child.type == Atom.TYPE_pssh) { - ParsableByteArray psshAtom = child.data; - psshAtom.setPosition(Atom.FULL_HEADER_SIZE); - UUID uuid = new UUID(psshAtom.readLong(), psshAtom.readLong()); - int dataSize = psshAtom.readInt(); - byte[] data = new byte[dataSize]; - psshAtom.readBytes(data, 0, dataSize); if (drmInitData == null) { drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4); } - drmInitData.put(uuid, data); + byte[] psshData = child.data.data; + drmInitData.put(PsshAtomUtil.parseUuid(psshData), psshData); } } if (drmInitData != null) { diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp4/PsshAtomUtil.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/PsshAtomUtil.java new file mode 100644 index 0000000000..03b910d7f7 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp4/PsshAtomUtil.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.extractor.mp4; + +import com.google.android.exoplayer.util.ParsableByteArray; + +import java.nio.ByteBuffer; +import java.util.UUID; + +/** + * Utility methods for handling PSSH atoms. + */ +public final class PsshAtomUtil { + + private PsshAtomUtil() {} + + /** + * Builds a PSSH atom for a given {@link UUID} containing the given scheme specific data. + * + * @param uuid The UUID of the scheme. + * @param data The scheme specific data. + * @return The PSSH atom. + */ + public static byte[] buildPsshAtom(UUID uuid, byte[] data) { + int psshBoxLength = Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */ + data.length; + ByteBuffer psshBox = ByteBuffer.allocate(psshBoxLength); + psshBox.putInt(psshBoxLength); + psshBox.putInt(Atom.TYPE_pssh); + psshBox.putInt(0 /* version=0, flags=0 */); + psshBox.putLong(uuid.getMostSignificantBits()); + psshBox.putLong(uuid.getLeastSignificantBits()); + psshBox.putInt(data.length); + psshBox.put(data); + return psshBox.array(); + } + + /** + * Parses the UUID from a PSSH atom. + *

+ * The UUID is only parsed if the data is a valid PSSH atom. + * + * @param atom The atom to parse. + * @return The parsed UUID. Null if the data is not a valid PSSH atom. + */ + public static UUID parseUuid(byte[] atom) { + ParsableByteArray atomData = new ParsableByteArray(atom); + if (!isPsshAtom(atomData, null)) { + return null; + } + atomData.setPosition(Atom.FULL_HEADER_SIZE); + return new UUID(atomData.readLong(), atomData.readLong()); + } + + /** + * Parses the scheme specific data from a PSSH atom. + *

+ * The scheme specific data is only parsed if the data is a valid PSSH atom matching the given + * UUID, or if the data is a valid PSSH atom of any type in the case that the passed UUID is null. + * + * @param atom The atom to parse. + * @param uuid The required UUID of the PSSH atom, or null to accept any UUID. + * @return The parsed scheme specific data. Null if the data is not a valid PSSH atom or if its + * UUID does not match the one provided. + */ + public static byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) { + ParsableByteArray atomData = new ParsableByteArray(atom); + if (!isPsshAtom(atomData, uuid)) { + return null; + } + atomData.setPosition(Atom.FULL_HEADER_SIZE + 16 /* UUID */); + int dataSize = atomData.readInt(); + byte[] data = new byte[dataSize]; + atomData.readBytes(data, 0, dataSize); + return data; + } + + private static boolean isPsshAtom(ParsableByteArray atomData, UUID uuid) { + if (atomData.limit() < Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */) { + // Data too short. + return false; + } + atomData.setPosition(0); + int atomSize = atomData.readInt(); + if (atomSize != atomData.bytesLeft() + 4) { + // Not an atom, or incorrect atom size. + return false; + } + int atomType = atomData.readInt(); + if (atomType != Atom.TYPE_pssh) { + // Not an atom, or incorrect atom type. + return false; + } + atomData.setPosition(Atom.FULL_HEADER_SIZE); + if (uuid == null) { + atomData.skipBytes(16); + } else if (atomData.readLong() != uuid.getMostSignificantBits() + || atomData.readLong() != uuid.getLeastSignificantBits()) { + // UUID doesn't match. + return false; + } + int dataSize = atomData.readInt(); + if (dataSize != atomData.bytesLeft()) { + // Incorrect dataSize. + return false; + } + return true; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java index 15186ddd88..a0bc0e7926 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer.smoothstreaming; import com.google.android.exoplayer.ParserException; +import com.google.android.exoplayer.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement; import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement; @@ -423,7 +424,7 @@ public class SmoothStreamingManifestParser implements UriLoadable.Parser