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.
This commit is contained in:
Oliver Woodman 2015-11-25 16:47:05 +00:00
parent ad7237b5d0
commit 7e2fffe74d
10 changed files with 107 additions and 68 deletions

View File

@ -21,6 +21,7 @@ import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.drm.DrmInitData; 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.ChunkIndex;
import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.extractor.webm.StreamBuilder.ContentEncodingSettings; import com.google.android.exoplayer.extractor.webm.StreamBuilder.ContentEncodingSettings;
@ -237,8 +238,12 @@ public final class WebmExtractorTest extends InstrumentationTestCase {
assertIndex(DEFAULT_TIMECODE_SCALE, 1); assertIndex(DEFAULT_TIMECODE_SCALE, 1);
DrmInitData drmInitData = extractorOutput.drmInitData; DrmInitData drmInitData = extractorOutput.drmInitData;
assertNotNull(drmInitData); assertNotNull(drmInitData);
android.test.MoreAsserts.assertEquals(TEST_ENCRYPTION_KEY_ID, drmInitData.get(WIDEVINE_UUID)); SchemeInitData widevineInitData = drmInitData.get(WIDEVINE_UUID);
android.test.MoreAsserts.assertEquals(TEST_ENCRYPTION_KEY_ID, drmInitData.get(ZERO_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 { public void testPrepareThreeCuePoints() throws IOException, InterruptedException {

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer;
import com.google.android.exoplayer.SampleSource.SampleSourceReader; import com.google.android.exoplayer.SampleSource.SampleSourceReader;
import com.google.android.exoplayer.drm.DrmInitData; 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.ExtractorSampleSource;
import com.google.android.exoplayer.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
@ -273,10 +274,10 @@ public final class FrameworkSampleSource implements SampleSource, SampleSourceRe
if (psshInfo == null || psshInfo.isEmpty()) { if (psshInfo == null || psshInfo.isEmpty()) {
return null; return null;
} }
DrmInitData.Mapped drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4); DrmInitData.Mapped drmInitData = new DrmInitData.Mapped();
for (UUID uuid : psshInfo.keySet()) { for (UUID uuid : psshInfo.keySet()) {
byte[] psshAtom = PsshAtomUtil.buildPsshAtom(uuid, psshInfo.get(uuid)); byte[] psshAtom = PsshAtomUtil.buildPsshAtom(uuid, psshInfo.get(uuid));
drmInitData.put(uuid, psshAtom); drmInitData.put(uuid, new SchemeInitData(MimeTypes.VIDEO_MP4, psshAtom));
} }
return drmInitData; return drmInitData;
} }

View File

@ -1054,8 +1054,6 @@ public class DashChunkSource implements ChunkSource, Output {
} }
private static DrmInitData getDrmInitData(AdaptationSet adaptationSet) { 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()) { if (adaptationSet.contentProtections.isEmpty()) {
return null; return null;
} else { } else {
@ -1064,7 +1062,7 @@ public class DashChunkSource implements ChunkSource, Output {
ContentProtection contentProtection = adaptationSet.contentProtections.get(i); ContentProtection contentProtection = adaptationSet.contentProtections.get(i);
if (contentProtection.uuid != null && contentProtection.data != null) { if (contentProtection.uuid != null && contentProtection.data != null) {
if (drmInitData == null) { if (drmInitData == null) {
drmInitData = new DrmInitData.Mapped(drmInitMimeType); drmInitData = new DrmInitData.Mapped();
} }
drmInitData.put(contentProtection.uuid, contentProtection.data); drmInitData.put(contentProtection.uuid, contentProtection.data);
} }

View File

@ -15,10 +15,10 @@
*/ */
package com.google.android.exoplayer.dash.mpd; 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.Assertions;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import java.util.Arrays;
import java.util.UUID; import java.util.UUID;
/** /**
@ -37,16 +37,16 @@ public class ContentProtection {
public final UUID uuid; 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 schemeUriId Identifies the content protection scheme.
* @param uuid The UUID of the protection scheme, if known. May be null. * @param uuid The UUID of the protection scheme, if known. May be null.
* @param data Protection scheme specific initialization data. 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.schemeUriId = Assertions.checkNotNull(schemeUriId);
this.uuid = uuid; this.uuid = uuid;
this.data = data; this.data = data;
@ -64,20 +64,14 @@ public class ContentProtection {
ContentProtection other = (ContentProtection) obj; ContentProtection other = (ContentProtection) obj;
return schemeUriId.equals(other.schemeUriId) return schemeUriId.equals(other.schemeUriId)
&& Util.areEqual(uuid, other.uuid) && Util.areEqual(uuid, other.uuid)
&& Arrays.equals(data, other.data); && Util.areEqual(data, other.data);
} }
@Override @Override
public int hashCode() { public int hashCode() {
int hashCode = 1; int hashCode = schemeUriId.hashCode();
hashCode = (37 * hashCode) + (uuid != null ? uuid.hashCode() : 0);
hashCode = hashCode * 37 + schemeUriId.hashCode(); hashCode = (37 * hashCode) + (data != null ? data.hashCode() : 0);
if (uuid != null) {
hashCode = hashCode * 37 + uuid.hashCode();
}
if (data != null) {
hashCode = hashCode * 37 + Arrays.hashCode(data);
}
return hashCode; return hashCode;
} }

View File

@ -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.SegmentTemplate;
import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTimelineElement; import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTimelineElement;
import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase; 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.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer.upstream.UriLoadable; import com.google.android.exoplayer.upstream.UriLoadable;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
@ -317,23 +318,24 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri");
UUID uuid = null; UUID uuid = null;
byte[] psshAtom = null; SchemeInitData data = null;
do { do {
xpp.next(); xpp.next();
// The cenc:pssh element is defined in 23001-7:2015 // The cenc:pssh element is defined in 23001-7:2015
if (ParserUtil.isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) { if (ParserUtil.isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) {
psshAtom = Base64.decode(xpp.getText(), Base64.DEFAULT); data = new SchemeInitData(MimeTypes.VIDEO_MP4,
uuid = PsshAtomUtil.parseUuid(psshAtom); Base64.decode(xpp.getText(), Base64.DEFAULT));
uuid = PsshAtomUtil.parseUuid(data.data);
if (uuid == null) { if (uuid == null) {
throw new ParserException("Invalid pssh atom in cenc:pssh element"); throw new ParserException("Invalid pssh atom in cenc:pssh element");
} }
} }
} while (!ParserUtil.isEndTag(xpp, "ContentProtection")); } while (!ParserUtil.isEndTag(xpp, "ContentProtection"));
return buildContentProtection(schemeIdUri, uuid, data);
return buildContentProtection(schemeIdUri, uuid, psshAtom);
} }
protected ContentProtection buildContentProtection(String schemeIdUri, UUID uuid, byte[] data) { protected ContentProtection buildContentProtection(String schemeIdUri, UUID uuid,
SchemeInitData data) {
return new ContentProtection(schemeIdUri, uuid, data); return new ContentProtection(schemeIdUri, uuid, data);
} }

View File

@ -15,25 +15,19 @@
*/ */
package com.google.android.exoplayer.drm; package com.google.android.exoplayer.drm;
import com.google.android.exoplayer.util.Assertions;
import android.media.MediaDrm; import android.media.MediaDrm;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID; 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 { public interface DrmInitData {
/**
* The container mime type.
*/
public final String mimeType;
public DrmInitData(String mimeType) {
this.mimeType = mimeType;
}
/** /**
* Retrieves initialization data for a given DRM scheme, specified by its UUID. * 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. * @param schemeUuid The DRM scheme's UUID.
* @return The initialization data for the scheme, or null if the scheme is not supported. * @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. * 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<UUID, byte[]> schemeData; private final Map<UUID, SchemeInitData> schemeData;
public Mapped(String mimeType) { public Mapped() {
super(mimeType);
schemeData = new HashMap<>(); schemeData = new HashMap<>();
} }
@Override @Override
public byte[] get(UUID schemeUuid) { public SchemeInitData get(UUID schemeUuid) {
return schemeData.get(schemeUuid); return schemeData.get(schemeUuid);
} }
@ -64,10 +57,10 @@ public abstract class DrmInitData {
* Inserts scheme specific initialization data. * Inserts scheme specific initialization data.
* *
* @param schemeUuid The scheme UUID. * @param schemeUuid The scheme UUID.
* @param data The corresponding initialization data. * @param schemeInitData The corresponding initialization data.
*/ */
public void put(UUID schemeUuid, byte[] data) { public void put(UUID schemeUuid, SchemeInitData schemeInitData) {
schemeData.put(schemeUuid, data); 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. * 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) { public Universal(SchemeInitData data) {
super(mimeType);
this.data = data; this.data = data;
} }
@Override @Override
public byte[] get(UUID schemeUuid) { public SchemeInitData get(UUID schemeUuid) {
return data; 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);
}
}
} }

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.drm; 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.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
@ -103,8 +104,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
private int state; private int state;
private MediaCrypto mediaCrypto; private MediaCrypto mediaCrypto;
private Exception lastException; private Exception lastException;
private String mimeType; private SchemeInitData schemeInitData;
private byte[] schemeData;
private byte[] sessionId; private byte[] sessionId;
/** /**
@ -273,20 +273,19 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
requestHandlerThread.start(); requestHandlerThread.start();
postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
} }
if (schemeData == null) { if (schemeInitData == null) {
mimeType = drmInitData.mimeType; schemeInitData = drmInitData.get(uuid);
schemeData = drmInitData.get(uuid); if (schemeInitData == null) {
if (schemeData == null) {
onError(new IllegalStateException("Media does not support uuid: " + uuid)); onError(new IllegalStateException("Media does not support uuid: " + uuid));
return; return;
} }
if (Util.SDK_INT < 21) { if (Util.SDK_INT < 21) {
// Prior to L the Widevine CDM required data to be extracted from the PSSH atom. // 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) { if (psshData == null) {
// Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
} else { } else {
schemeData = psshData; schemeInitData = new SchemeInitData(schemeInitData.mimeType, psshData);
} }
} }
} }
@ -307,7 +306,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
postRequestHandler = null; postRequestHandler = null;
requestHandlerThread.quit(); requestHandlerThread.quit();
requestHandlerThread = null; requestHandlerThread = null;
schemeData = null; schemeInitData = null;
mediaCrypto = null; mediaCrypto = null;
lastException = null; lastException = null;
if (sessionId != null) { if (sessionId != null) {
@ -369,7 +368,7 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
private void postKeyRequest() { private void postKeyRequest() {
KeyRequest keyRequest; KeyRequest keyRequest;
try { try {
keyRequest = mediaDrm.getKeyRequest(sessionId, schemeData, mimeType, keyRequest = mediaDrm.getKeyRequest(sessionId, schemeInitData.data, schemeInitData.mimeType,
MediaDrm.KEY_TYPE_STREAMING, optionalKeyRequestParameters); MediaDrm.KEY_TYPE_STREAMING, optionalKeyRequestParameters);
postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget(); postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
} catch (NotProvisionedException e) { } catch (NotProvisionedException e) {

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer.extractor.mp4;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.drm.DrmInitData; 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.ChunkIndex;
import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.extractor.ExtractorInput;
@ -292,10 +293,11 @@ public final class FragmentedMp4Extractor implements Extractor {
LeafAtom child = moovChildren.get(i); LeafAtom child = moovChildren.get(i);
if (child.type == Atom.TYPE_pssh) { if (child.type == Atom.TYPE_pssh) {
if (drmInitData == null) { if (drmInitData == null) {
drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4); drmInitData = new DrmInitData.Mapped();
} }
byte[] psshData = child.data.data; 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) { if (drmInitData != null) {

View File

@ -19,6 +19,7 @@ import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.drm.DrmInitData; 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.ChunkIndex;
import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput; 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"); throw new ParserException("Encrypted Track found but ContentEncKeyID was not found");
} }
if (!sentDrmInitData) { if (!sentDrmInitData) {
extractorOutput.drmInitData( extractorOutput.drmInitData(new DrmInitData.Universal(
new DrmInitData.Universal(MimeTypes.VIDEO_WEBM, currentTrack.encryptionKeyId)); new SchemeInitData(MimeTypes.VIDEO_WEBM, currentTrack.encryptionKeyId)));
sentDrmInitData = true; sentDrmInitData = true;
} }
} }

View File

@ -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.FormatEvaluator.Evaluation;
import com.google.android.exoplayer.chunk.MediaChunk; import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.drm.DrmInitData; 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.FragmentedMp4Extractor;
import com.google.android.exoplayer.extractor.mp4.Track; import com.google.android.exoplayer.extractor.mp4.Track;
import com.google.android.exoplayer.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer.extractor.mp4.TrackEncryptionBox;
@ -144,8 +145,9 @@ public class SmoothStreamingChunkSource implements ChunkSource,
byte[] keyId = getProtectionElementKeyId(protectionElement.data); byte[] keyId = getProtectionElementKeyId(protectionElement.data);
trackEncryptionBoxes = new TrackEncryptionBox[1]; trackEncryptionBoxes = new TrackEncryptionBox[1];
trackEncryptionBoxes[0] = new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId); trackEncryptionBoxes[0] = new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId);
drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4); drmInitData = new DrmInitData.Mapped();
drmInitData.put(protectionElement.uuid, protectionElement.data); drmInitData.put(protectionElement.uuid,
new SchemeInitData(MimeTypes.VIDEO_MP4, protectionElement.data));
} else { } else {
trackEncryptionBoxes = null; trackEncryptionBoxes = null;
drmInitData = null; drmInitData = null;