深入理解DRM——MediaDRM和MediaCrypto
Posted zhanghui_cuc
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解DRM——MediaDRM和MediaCrypto相关的知识,希望对你有一定的参考价值。
文章目录
How MediaDRM Works?
下面以ExoPlayer代码为例介绍Widevine Modular中所用的MediaDRM。
总结下来有以下几步
- Step1.根据UUID创建MeidaDRM实例
- Step2.Open Session
- Step3. add keys: MediaDrm的getKeyRequest和provideKeyResponse
- Step4. 创建MediaCrypto对象,注册到MediaCodec中
- Step5. 拥有了MediaExtractor,MediaCodec,MediaCrypto之后,就开始从extractor中readSampleData,通过queueSecureInputBuffer送给Decoder,再使用MediaCodec的decrypt方法解密内容.
- 最后close session
根据UUID创建MeidaDRM实例
com/google/android/exoplayer/drm/StreamingDrmSessionManager.java
try {
mediaDrm = new MediaDrm(uuid);
...
}
frameworks/av/media/libmediaplayerservice/Drm.cpp@createPlugin(const uint8_t uuid[16])
↓
/*
* 在/vendor/lib/mediadrm目录下搜索支持对应uuid所指定drm的plugin,这个搜索的路径无论哪个平台都一样
*/
Drm.cpp@findFactoryForScheme(const uint8_t uuid[16])
↓
/*
*确认上述目录下的so库是否实现了createDrmFactory方法
*createDrmFactory constructs and returns an instance of a DrmFactory object.
*When a match is found, the DrmEngine's createDrmPlugin methods is used to create *DrmPlugin instances to support that DRM scheme.
*/
Drm.cpp@loadLibraryForScheme(const String8 &path, const uint8_t uuid[16])
↓
/*
*WVDrmFactory继承自DrmFactory
*在这里实例化WVDrmPlugin, WVDrmPlugin继承自DrmPlugin
*/
vendor/widevine/libwvdrmengine/src/WVDrmFactory.cpp@createDrmPlugin(const uint8_t uuid[16], DrmPlugin** plugin)
↓
vendor/widevine/libwvdrmengine/mediadrm/src/WVDrmPlugin.cpp@WVDrmPlugin(const sp<WvContentDecryptionModule>& cdm, WVGenericCryptoInterface* crypto)
上面提到了DrmFactory和DrmPlugin,画一个简单的类图如下
可以看到,DrmFactory和DrmPlugin对应具体的Drm方法都有具体的子类,DrmFactory除了判断当前Drm方法是否受支持外,还用于实例化DrmPlugin,而DrmPlugin完成具体的解密\KeyRequest等等工作。
Open Session
为后续操作生成唯一的session id。
com/google/android/exoplayer/drm/StreamingDrmSessionManager.java
private void openInternal(boolean allowProvisioning) {
….
//Open a new session with the MediaDrm object. A session ID is returned.
sessionId = mediaDrm.openSession();
….
}
}
再往下的流程如下图所示
图中,在cdmSession的QueryKeyControlInfo方法中会获得OEMCryptoSessionId并且写到KeyControlBlockMap中去。
Add keys
也就是MediaDrm的getKeyRequest和provideKeyResponse。
先来看getKeyRequest
com/google/android/exoplayer/drm/StreamingDrmSessionManager.java
private void postKeyRequest(byte[] scope, int keyType) {
try {
KeyRequest keyRequest = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, keyType,
optionalKeyRequestParameters);
postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
} catch (Exception e) {
onKeysError(e);
}
}
再往下的流程如下图所示
CdmLicenseType有三种:offline:离线License, streaming:在线License, release:把offline时保存的key release掉.相应的如果是streaming或者offline的key type,都会与对应的session id绑定,如果是release type的,绑定需要release的key set id,此时没有session id。
在InitializationData的构造方法中,根据mimeType来判断是什么类型的流,如果是video/mp4或audio/mp4,则认为是CENC流,然后去读mp4box,从pssh box读drmInitData,如果mimeType是webm的,则认为是Webm的流。
EnableTimer方法开始计时,用于统计key是否过期。
再来看provideKeyResponse
com/google/android/exoplayer/drm/StreamingDrmSessionManager.java
byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
再往下的流程如下图所示
图中,provideKeyResponse方法的入参就是服务器返回的byte[]。
在CdmLicense的ExtractContentKeys(License)方法中,就是从license中读key data\\iv 和 key control data\\iv。
在cryptoSession的LoadKeys中,就是把CryptoKeys赋值给OEMCrypto_KeyObject。
创建MediaCrypto对象,注册到MediaCodec中
使用UUID和SessionID创建MediaCrypto对象.使用MediaCodec.configure(MediaFormat, Surface, MediaCrypto, int)方法将MeidaCrypto对象注册到MediaCodec中,从而让Codec可以解密内容。
先来看MediaCrypto初始化的部分
com/google/android/exoplayer/drm/StreamingDrmSessionManager.java
private void openInternal(boolean allowProvisioning) {
try {
sessionId = mediaDrm.openSession();
mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId);
state = STATE_OPENED;
….
}
再往下的流程如下图所示
图中isCryptoSchemeSupport就是看传过来的uuid是不是widevine对应的uuid。
loadLibraryForScheme(path, uuid)就是去vendor/lib/mediadrm目录下找so库,看so库是否有实现createCryptoFactory方法。
接下来看Mediacodec.configure(mediaCrypto)的流程
com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
MediaCrypto crypto) throws DecoderQueryException {
codecMaxValues = getCodecMaxValues(codecInfo, format, streamFormats);
MediaFormat mediaFormat = getMediaFormat(format, codecMaxValues, deviceNeedsAutoFrcWorkaround,
tunnelingAudiosessionId);
codec.configure(mediaFormat, surface, crypto, 0);
if (Util.SDK_INT >= 23 && tunneling) {
tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec);
}
}
frameworks/av/media/libstagefright/MediaCodec.cpp
status_t MediaCodec::configure(
const sp<AMessage> &format,
const sp<Surface> &surface,
const sp<ICrypto> &crypto,
uint32_t flags) {
…
send msg: kWhatConfigure
...
}
onkWhatConfigure
mCrypto = static_cast<ICrypto *>(crypto);
开始解密
拥有了MediaExtractor,MediaCodec,MediaCrypto之后,就开始从extractor中readSampleData,通过queueSecureInputBuffer送给Decoder,再使用MediaCodec的decrypt方法解密内容。
上述步骤循环往复
从Sample中读出包括keyid, iv等在内的cryptoinfo
com/google/android/exoplayer2/extractor/DefaultTrackOutput.java
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean loadingFinished,
long decodeOnlyUntilUs) {
…
// Read encryption data if the sample is encrypted.
if (buffer.isEncrypted()) { //以fmp4为例,这里只要extractor读到了跟加密相关的box,就会把对应的flag写true
readEncryptionData(buffer, extrasHolder);
}
…
}
private void readEncryptionData(DecoderInputBuffer buffer, BufferExtrasHolder extrasHolder) {
…
// Populate the cryptoInfo.
buffer.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes,
extrasHolder.encryptionKeyId, buffer.cryptoInfo.iv, C.CRYPTO_MODE_AES_CTR);
…
}
public final class CryptoInfo {
/**
* @see android.media.MediaCodec.CryptoInfo#iv
*/
public byte[] iv;
/**
* @see android.media.MediaCodec.CryptoInfo#key
*/
public byte[] key;
/**
* @see android.media.MediaCodec.CryptoInfo#mode
*/
@C.CryptoMode
public int mode;
/**
* @see android.media.MediaCodec.CryptoInfo#numBytesOfClearData
*/
public int[] numBytesOfClearData;
/**
* @see android.media.MediaCodec.CryptoInfo#numBytesOfEncryptedData
*/
public int[] numBytesOfEncryptedData;
/**
* @see android.media.MediaCodec.CryptoInfo#numSubSamples
*/
public int numSubSamples;
…
}
在feedInputBuffer的时候调用queueSecureInputBuffer
if (bufferEncrypted) {
MediaCodec.CryptoInfo cryptoInfo = getFrameworkCryptoInfo(buffer,
adaptiveReconfigurationBytes);
codec.queueSecureInputBuffer(inputIndex, 0, cryptoInfo, presentationTimeUs, 0);
} else {
codec.queueInputBuffer(inputIndex, 0, buffer.data.limit(), presentationTimeUs, 0);
}
再往下的流程如下所示
图中在FindSessionForKey方法中是利用kid与sid的对应关系找到相应的session,这个对应关系在key response的时候就建立起来了,在sessionSharing中也是这样的逻辑.如果找不到load了相应key的session,则返回NEED_KEY,并且报错"unable to find session"。
在cryptoSession的decrypt方法中会分两种情况:若是清流,则直接调用OEMCrypto_CopyBuffer,并且会将SET_VIDEO_PLAY_START_FLAG置位true,因为根据CENC标准,在加密流起始位置是一小段清流;若是加密流,才会调用OEMCrypto_DecryptCTR.这里如果发现key过期了,会返回NEED_KEY.。
Widevine DRM Testing
验证设备DRM工作是否正常,也有很多中方法:
- GTS测试中包含了对两种widevine的测项
- widevine/libwvdrmengine/test/demo目录下还有一个ExoPlayerDemo.apk专供WidevineDRM测试,包含的测项基本和google原生exoplayer demo包含的一样
- Reference Implementation
为OEMCrypto的所有特性都提供了参考实现,可以用这部分代码做调试或测试,但是其中没有production key也没有level 1 security,使用如下
cd $ANDROID_BUILD_TOP/vendor/widevine/libwvdrmengine/oemcrypto/mock mm
adb root
adb remount
adb push $OUT/system/vendor/lib/liboemcrypto.so /system/vendor/lib
- 进行unit test
cd $ANDROID_BUILD_TOP/external/gtest
mm
# Build the unit tests for oemcrypto.so:
cd $ANDROID_BUILD_TOP/vendor/widevine/libwvdrmengine/oemcrypto/test mm
Run the existing unit tests:
cd $ANDROID_BUILD_TOP
adb root
adb remount
adb push $OUT/system/bin/oemcrypto_test /system/bin
adb shell /system/bin/oemcrypto_test
- Build Test APK
这个APK利用MediaDRM API完成key request,在CDM中响应key response,通过OEMCrypto API将key load到TEE中。这个APK没有界面,通过log可以判断key是否成功load。
cd $ANDROID_BUILD_TOP/vendor/widevine/libwvdrmengine/test/java mm
adb install MediaDrmAPITest.apk
欢迎关注我的公众号灰度五十,分享各类音视频、移动开发知识~
文章帮到你了?可以扫描如下二维码进行打赏,打赏多少您随意~
以上是关于深入理解DRM——MediaDRM和MediaCrypto的主要内容,如果未能解决你的问题,请参考以下文章