How MediaDRM Works?

The following uses the ExoPlayer code as an example of the MediaDRM used in Widevine Modular.

The summary comes down to the following steps

  • Step1. Create a MeidaDRM instance based on the UUID
  • Step2.Open Session
  • Step3. add keys: MediaDrm getKeyRequest and provideKeyResponse
  • Step4. Create a MediaCrypto object and register it with MediaCodec
  • Step5. After having MediaExtractor, MediaCodec, MediaCrypto, readSampleData from the extractor is sent to the Decoder through queueSecureInputBuffer. MediaCodec’s decrypt method is then used to decrypt the content.
  • Finally close the session

Create MeidaDRM instance based on UUID

com/google/android/exoplayer/drm/StreamingDrmSessionManager.java
try {
 mediaDrm = newMediaDrm(uuid); . } frameworks/av/media/libmediaplayerservice/Drm.cpp@createPlugin(const uint8_t uuid[16])/* * Search /vendor/lib/mediadrm for plugins that support the DRM specified by the uUID. The search path is the same for all platforms */
Drm.cpp@findFactoryForScheme(const uint8_t uuid[16])CreateDrmFactory constructs an emotional state and returns an instance of a DrmFactory object. Constructs an emotional state and returns an instance of a DrmFactory. Constructs an emotional state 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 inherits from DrmFactory */
vendor/widevine/libwvdrmengine/src/WVDrmFactory.cpp@createDrmPlugin(const uint8_t uuid[16], DrmPlugin** plugin)Left vendor/widevine/libwvdrmengine/mediadrm/SRC/WVDrmPlugin CPP@WVDrmPlugin(const sp<WvContentDecryptionModule>& cdm, WVGenericCryptoInterface* crypto)

Copy the code

With the DrmFactory and DrmPlugin mentioned above, draw a simple class diagram as followsAs you can see, both DrmFactory and DrmPlugin have specific subclasses for specific Drm methods. In addition to determining whether the current Drm method is supported, DrmFactory is also used to instantiate DrmPlugin, and DrmPlugin is used to decrypt \ KeyRequest, etc.

Open Session

Generate a unique session ID for subsequent operations.

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(); ... .}}Copy the code

The flow further down is shown belowIn the figure, the OEMCryptoSessionId is obtained in the QueryKeyControlInfo method of the cdmSession and written to the KeyControlBlockMap.

Add keys

So MediaDrm’s getKeyRequest and provideKeyResponse.

Start with 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); }}Copy the code

The flow further down is shown belowCdmLicenseType there are three types of CdmLicenseType: Offline: an offline License, streaming: an online License, and release: release the key saved in offline mode. If the key type is streaming or offline, it will be bound to the corresponding session ID. If the key type is release, the binding requires the release key set ID, and there is no session ID in this case.

In the InitializationData constructor, determine the type of stream according to mimeType. If it is video/mp4 or Audio /mp4, it is considered as CENC stream. Then read mp4Box, read drmInitData from PSSH box. If mimeType is webM, it is considered a WebM stream.

The EnableTimer method starts to count whether the key expires.

Look at provideKeyResponse

com/google/android/exoplayer/drm/StreamingDrmSessionManager.java
byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
Copy the code

The flow further down is shown belowIn the figure, the input parameter to the provideKeyResponse method is the byte[] returned by the server.

In the ExtractContentKeys(License) method of CdmLicense, it reads key data\iv and key Control data\iv from the License.

In the LoadKeys of a cryptoSession, you assign the CryptoKeys to the OEMCrypto_KeyObject.

Create a MediaCrypto object and register it with MediaCodec

Create MediaCrypto objects with UUID and SessionID. Use the Mediacodec. configure(MediaFormat, Surface, MediaCrypto, int) method to register MeidaCrypto objects with MediaCodec so that Codec can decrypt the content.

Let’s start with MediaCrypto initialization

com/google/android/exoplayer/drm/StreamingDrmSessionManager.java
private void openInternal(boolean allowProvisioning) {
 try{ sessionId = mediaDrm.openSession(); mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId); state = STATE_OPENED; ... .}Copy the code

The flow further down is shown belowIn the figure, isCryptoSchemeSupport is used to check whether the uUID passed is the uUID corresponding to widevine.

LoadLibraryForScheme (path, uuid) is to find the so library in vendor/lib/mediadrm and see if the so library implements createCryptoFactory.

Configure (mediaCrypto). 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,
        constSp <ICrypto> & Crypto, uint32_t flags) {... send msg: kWhatConfigure ... } onkWhatConfigure mCrypto = static_cast<ICrypto *>(crypto);Copy the code

Began to decrypt

After having MediaExtractor, MediaCodec, MediaCrypto, start readSampleData from extractor, send to Decoder via queueSecureInputBuffer, MediaCodec’s decrypt method is then used to decrypt the content.

The above steps repeat themselves

Read from the Sample including the 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()) {// Take fmp4 as an example, as long as the extractor reads the box related to encryption, it will write the corresponding flag truereadEncryptionData(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 intnumSubSamples; ... } is called at feedInputBufferqueueSecureInputBuffer
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);
}
Copy the code

The process further down is shown belowIn the figure, the mapping between kid and SID is used to find the corresponding session in the FindSessionForKey method. This mapping is established in the key response, and the same logic is used in sessionSharing. If the session with the corresponding key cannot be found, NEED_KEY is returned with “Unable to find session”.

There are two types of decrypt methods in cryptoSession: If the stream is clear, OEMCrypto_CopyBuffer is called directly, and SET_VIDEO_PLAY_START_FLAG is set to true, because according to CENC standards, a small stream is clear at the beginning of the encrypted stream. OEMCrypto_DecryptCTR is called only if the stream is encrypted. If the key is found to be expired, NEED_KEY. Is returned.

Widevine DRM Testing

There are many methods to verify that the device DRM works properly:

  • The GTS test includes two widevine tests
  • Widevine/libwvdrmengine/test/demo directory. There is also a ExoPlayerDemo apk for WidevineDRM test, contains the basic measurement and Google native exoplayer demo contains
  • Reference Implementation

Reference implementation is provided for all features of OEMCrypto. You can use this part of code for debugging or testing, but there is no Production key or Level 1 security in it. Use it as follows

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 
Copy the code
  • Conduct 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 
Copy the code
  • Build Test APK

The APK uses the MediaDRM API to complete the key request, responds to the key response in the CDM, and loads the key into the TEE via the OEMCrypto API. This APK has no interface, so you can check whether the key is loaded successfully through log.

cd $ANDROID_BUILD_TOP/vendor/widevine/libwvdrmengine/test/java mm
adb install MediaDrmAPITest.apk 
Copy the code