blob: 2663a7ec23ba4980cf197e6a9f92f950af768f8f [file] [log] [blame]
/*
* Copyright 2019 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 androidx.media2.player;
import static android.content.Context.KEYGUARD_SERVICE;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.app.DownloadManager;
import android.app.DownloadManager.Request;
import android.app.Instrumentation;
import android.app.KeyguardManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.media.MediaDrm;
import android.net.Uri;
import android.os.Build;
import android.os.PowerManager;
import android.os.SystemClock;
import android.util.Base64;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.WindowManager;
import androidx.annotation.CallSuper;
import androidx.media2.common.MediaItem;
import androidx.media2.common.UriMediaItem;
import androidx.media2.player.MediaPlayer2.DrmInfo;
import androidx.media2.player.TestUtils.Monitor;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.UUID;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Base class for DRM tests which use MediaPlayer2 to play audio or video.
*/
public class MediaPlayer2DrmTestBase {
protected static final int STREAM_RETRIES = 3;
protected Monitor mSetDataSourceCallCompleted = new Monitor();
protected Monitor mOnPreparedCalled = new Monitor();
protected Monitor mOnVideoSizeChangedCalled = new Monitor();
protected Monitor mOnPlaybackCompleted = new Monitor();
protected Monitor mOnDrmInfoCalled = new Monitor();
protected Monitor mOnDrmPreparedCalled = new Monitor();
protected int mCallStatus = MediaPlayer2.CALL_STATUS_NO_ERROR;
protected Context mContext;
protected Resources mResources;
protected MediaPlayer2 mPlayer = null;
protected MediaStubActivity mActivity;
protected Instrumentation mInstrumentation;
protected ExecutorService mExecutor;
protected MediaPlayer2.EventCallback mECb = null;
@Rule
public ActivityTestRule<MediaStubActivity> mActivityRule =
new ActivityTestRule<>(MediaStubActivity.class);
public PowerManager.WakeLock mScreenLock;
private KeyguardManager mKeyguardManager;
@Before
@CallSuper
public void setUp() throws Throwable {
mInstrumentation = InstrumentationRegistry.getInstrumentation();
mKeyguardManager = (KeyguardManager)
mInstrumentation.getTargetContext().getSystemService(KEYGUARD_SERVICE);
mActivity = mActivityRule.getActivity();
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
// Keep screen on while testing.
mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
mActivity.setTurnScreenOn(true);
mActivity.setShowWhenLocked(true);
mKeyguardManager.requestDismissKeyguard(mActivity, null);
}
});
mInstrumentation.waitForIdleSync();
try {
mActivityRule.runOnUiThread(new Runnable() {
public void run() {
mPlayer = MediaPlayer2.create(mActivity);
}
});
} catch (Throwable e) {
e.printStackTrace();
fail();
}
mContext = mInstrumentation.getTargetContext();
mResources = mContext.getResources();
mExecutor = Executors.newFixedThreadPool(2);
}
@After
@CallSuper
public void tearDown() throws Throwable {
if (mPlayer != null) {
mPlayer.close();
mPlayer = null;
}
mExecutor.shutdown();
mActivity = null;
}
private static class PrepareFailedException extends Exception {}
//////////////////////////////////////////////////////////////////////////////////////////
// Modular DRM
private static final String TAG = "MediaPlayer2DrmTestBase";
protected static final int PLAY_TIME_MS = 60 * 1000;
protected byte[] mKeySetId;
protected boolean mAudioOnly;
private static final byte[] CLEAR_KEY_CENC = {
(byte) 0x1a, (byte) 0x8a, (byte) 0x20, (byte) 0x95,
(byte) 0xe4, (byte) 0xde, (byte) 0xb2, (byte) 0xd2,
(byte) 0x9e, (byte) 0xc8, (byte) 0x16, (byte) 0xac,
(byte) 0x7b, (byte) 0xae, (byte) 0x20, (byte) 0x82
};
private static final UUID CLEARKEY_SCHEME_UUID =
new UUID(0x1077efecc0b24d02L, 0xace33c1e52e2fb4bL);
final byte[] mClearKeyPssh = hexStringToByteArray(
"0000003470737368" // BMFF box header (4 bytes size + 'pssh')
+ "01000000" // Full box header (version = 1 flags = 0)
+ "1077efecc0b24d02" // SystemID
+ "ace33c1e52e2fb4b"
+ "00000001" // Number of key ids
+ "60061e017e477e87" // Key id
+ "7e57d00d1ed00d1e"
+ "00000000" // Size of Data, must be zero
);
protected enum ModularDrmTestType {
V0_SYNC_TEST,
V1_ASYNC_TEST,
V2_SYNC_CONFIG_TEST,
V3_ASYNC_DRMPREPARED_TEST,
V4_SYNC_OFFLINE_KEY,
}
// TODO: After living on these tests for a while, we can consider grouping them based on
// the asset such that each asset is downloaded once and played back with multiple tests.
protected void playModularDrmVideoDownload(Uri uri, Uri path, int width, int height,
ModularDrmTestType testType) throws Exception {
final long downloadTimeOutSeconds = 600;
Log.i(TAG, "Downloading file:" + path);
MediaDownloadManager mediaDownloadManager = new MediaDownloadManager(mContext);
final long id = mediaDownloadManager.downloadFileWithRetries(
uri, path, downloadTimeOutSeconds, STREAM_RETRIES);
assertFalse("Download " + uri + " failed.", id == -1);
Uri file = mediaDownloadManager.getUriForDownloadedFile(id);
Log.i(TAG, "Downloaded file:" + path + " id:" + id + " uri:" + file);
try {
playModularDrmVideo(file, width, height, testType);
} finally {
mediaDownloadManager.removeFile(id);
}
}
protected void playModularDrmVideo(Uri uri, int width, int height,
ModularDrmTestType testType) throws Exception {
// Force gc for a clean start
System.gc();
playModularDrmVideoWithRetries(uri, width, height, PLAY_TIME_MS, testType);
}
protected void playModularDrmVideoWithRetries(Uri file, Integer width, Integer height,
int playTime, ModularDrmTestType testType) throws Exception {
// first the synchronous variation
boolean playedSuccessfully = false;
for (int i = 0; i < STREAM_RETRIES; i++) {
try {
Log.v(TAG, "playVideoWithRetries(" + testType + ") try " + i);
playLoadedModularDrmVideo(file, width, height, playTime, testType);
playedSuccessfully = true;
break;
} catch (PrepareFailedException e) {
// we can fail because of network issues, so try again
Log.w(TAG, "playVideoWithRetries(" + testType + ") failed on try " + i
+ ", trying playback again");
mPlayer.reset();
}
}
assertTrue("Stream did not play successfully after all attempts (syncDrmSetup)",
playedSuccessfully);
}
/**
* Play a video which has already been loaded with setMediaItem().
* The DRM setup is performed synchronously.
*
* @param file media item
* @param width width of the video to verify, or null to skip verification
* @param height height of the video to verify, or null to skip verification
* @param playTime length of time to play video, or 0 to play entire video
* @param testType test type
*/
private void playLoadedModularDrmVideo(final Uri file, final Integer width,
final Integer height, int playTime, ModularDrmTestType testType) throws Exception {
switch (testType) {
case V0_SYNC_TEST:
case V1_ASYNC_TEST:
case V2_SYNC_CONFIG_TEST:
case V3_ASYNC_DRMPREPARED_TEST:
playLoadedModularDrmVideo_Generic(file, width, height, playTime, testType);
break;
case V4_SYNC_OFFLINE_KEY:
playLoadedModularDrmVideo_V4_offlineKey(file, width, height, playTime);
break;
}
}
private void playLoadedModularDrmVideo_Generic(final Uri file, final Integer width,
final Integer height, int playTime, ModularDrmTestType testType) throws Exception {
final float volume = 0.5f;
mAudioOnly = (width == 0);
mCallStatus = MediaPlayer2.CALL_STATUS_NO_ERROR;
mECb = new MediaPlayer2.EventCallback() {
@Override
public void onVideoSizeChanged(MediaPlayer2 mp, MediaItem item, int w, int h) {
Log.v(TAG, "VideoSizeChanged" + " w:" + w + " h:" + h);
mOnVideoSizeChangedCalled.signal();
}
@Override
public void onError(MediaPlayer2 mp, MediaItem item, int what, int extra) {
fail("Media player had error " + what + " playing video");
}
@Override
public void onInfo(MediaPlayer2 mp, MediaItem item, int what, int extra) {
if (what == MediaPlayer2.MEDIA_INFO_PREPARED) {
mOnPreparedCalled.signal();
} else if (what == MediaPlayer2.MEDIA_INFO_DATA_SOURCE_END) {
Log.v(TAG, "playLoadedVideo: onInfo_PlaybackComplete");
mOnPlaybackCompleted.signal();
}
}
@Override
public void onCallCompleted(MediaPlayer2 mp, MediaItem item,
int what, int status) {
if (what == MediaPlayer2.CALL_COMPLETED_SET_DATA_SOURCE) {
mCallStatus = status;
mSetDataSourceCallCompleted.signal();
}
}
};
mPlayer.setEventCallback(mExecutor, mECb);
Log.v(TAG, "playLoadedVideo: setMediaItem()");
mPlayer.setMediaItem(new UriMediaItem.Builder(file).build());
mSetDataSourceCallCompleted.waitForSignal();
if (mCallStatus != MediaPlayer2.CALL_STATUS_NO_ERROR) {
throw new PrepareFailedException();
}
SurfaceHolder surfaceHolder = mActivity.getSurfaceHolder();
surfaceHolder.setKeepScreenOn(true);
mPlayer.setSurface(surfaceHolder.getSurface());
try {
switch (testType) {
case V0_SYNC_TEST:
preparePlayerAndDrm_V0_syncDrmSetup();
break;
case V1_ASYNC_TEST:
preparePlayerAndDrm_V1_asyncDrmSetup();
break;
case V2_SYNC_CONFIG_TEST:
preparePlayerAndDrm_V2_syncDrmSetupPlusConfig();
break;
case V3_ASYNC_DRMPREPARED_TEST:
preparePlayerAndDrm_V3_asyncDrmSetupPlusDrmPreparedListener();
break;
}
} catch (IOException e) {
e.printStackTrace();
throw new PrepareFailedException();
}
Log.v(TAG, "playLoadedVideo: play()");
mPlayer.play();
if (!mAudioOnly) {
mOnVideoSizeChangedCalled.waitForSignal();
}
mPlayer.setPlayerVolume(volume);
// waiting to complete
if (playTime == 0) {
Log.v(TAG, "playLoadedVideo: waiting for playback completion");
mOnPlaybackCompleted.waitForSignal();
} else {
Log.v(TAG, "playLoadedVideo: waiting while playing for " + playTime);
mOnPlaybackCompleted.waitForSignal(playTime);
}
try {
Log.v(TAG, "playLoadedVideo: releaseDrm");
mPlayer.releaseDrm();
} catch (Exception e) {
e.printStackTrace();
throw new PrepareFailedException();
}
}
private void preparePlayerAndDrm_V0_syncDrmSetup() throws Exception {
Log.v(TAG, "preparePlayerAndDrm_V0: calling prepare()");
mPlayer.prepare();
mOnPreparedCalled.waitForSignal();
if (mCallStatus != MediaPlayer2.CALL_STATUS_NO_ERROR) {
throw new IOException();
}
DrmInfo drmInfo = mPlayer.getDrmInfo();
if (drmInfo != null) {
setupDrm(drmInfo, true /* prepareDrm */, true /* synchronousNetworking */,
MediaDrm.KEY_TYPE_STREAMING);
Log.v(TAG, "preparePlayerAndDrm_V0: setupDrm done!");
}
}
private void preparePlayerAndDrm_V1_asyncDrmSetup() throws InterruptedException {
final AtomicBoolean asyncSetupDrmError = new AtomicBoolean(false);
mPlayer.setDrmEventCallback(mExecutor, new MediaPlayer2.DrmEventCallback() {
@Override
public void onDrmInfo(MediaPlayer2 mp, MediaItem item, DrmInfo drmInfo) {
Log.v(TAG, "preparePlayerAndDrm_V1: onDrmInfo" + drmInfo);
// in the callback (async mode) so handling exceptions here
try {
setupDrm(drmInfo, true /* prepareDrm */, true /* synchronousNetworking */,
MediaDrm.KEY_TYPE_STREAMING);
} catch (Exception e) {
Log.v(TAG, "preparePlayerAndDrm_V1: setupDrm EXCEPTION " + e);
asyncSetupDrmError.set(true);
}
mOnDrmInfoCalled.signal();
Log.v(TAG, "preparePlayerAndDrm_V1: onDrmInfo done!");
}
});
Log.v(TAG, "preparePlayerAndDrm_V1: calling prepare()");
mPlayer.prepare();
mOnDrmInfoCalled.waitForSignal();
// Waiting till the player is prepared
mOnPreparedCalled.waitForSignal();
// to handle setupDrm error (async) in the main thread rather than the callback
if (asyncSetupDrmError.get()) {
fail("preparePlayerAndDrm_V1: setupDrm");
}
}
private void preparePlayerAndDrm_V2_syncDrmSetupPlusConfig() throws Exception {
mPlayer.setOnDrmConfigHelper(new MediaPlayer2.OnDrmConfigHelper() {
@Override
public void onDrmConfig(MediaPlayer2 mp, MediaItem item) {
String widevineSecurityLevel3 = "L3";
String securityLevelProperty = "securityLevel";
try {
String level = mp.getDrmPropertyString(securityLevelProperty);
Log.v(TAG, "preparePlayerAndDrm_V2: getDrmPropertyString: "
+ securityLevelProperty + " -> " + level);
mp.setDrmPropertyString(securityLevelProperty, widevineSecurityLevel3);
level = mp.getDrmPropertyString(securityLevelProperty);
Log.v(TAG, "preparePlayerAndDrm_V2: getDrmPropertyString: "
+ securityLevelProperty + " -> " + level);
} catch (MediaPlayer2.NoDrmSchemeException e) {
Log.v(TAG, "preparePlayerAndDrm_V2: NoDrmSchemeException");
} catch (Exception e) {
Log.v(TAG, "preparePlayerAndDrm_V2: onDrmConfig EXCEPTION " + e);
}
}
});
Log.v(TAG, "preparePlayerAndDrm_V2: calling prepare()");
mPlayer.prepare();
mOnPreparedCalled.waitForSignal();
if (mCallStatus != MediaPlayer2.CALL_STATUS_NO_ERROR) {
throw new IOException();
}
DrmInfo drmInfo = mPlayer.getDrmInfo();
if (drmInfo != null) {
setupDrm(drmInfo, true /* prepareDrm */, true /* synchronousNetworking */,
MediaDrm.KEY_TYPE_STREAMING);
Log.v(TAG, "preparePlayerAndDrm_V2: setupDrm done!");
}
}
private void preparePlayerAndDrm_V3_asyncDrmSetupPlusDrmPreparedListener()
throws InterruptedException {
final AtomicBoolean asyncSetupDrmError = new AtomicBoolean(false);
mPlayer.setDrmEventCallback(mExecutor, new MediaPlayer2.DrmEventCallback() {
@Override
public void onDrmInfo(MediaPlayer2 mp, MediaItem item, DrmInfo drmInfo) {
Log.v(TAG, "preparePlayerAndDrm_V3: onDrmInfo" + drmInfo);
// DRM preperation
List<UUID> supportedSchemes = drmInfo.getSupportedSchemes();
if (supportedSchemes.isEmpty()) {
Log.e(TAG, "preparePlayerAndDrm_V3: onDrmInfo: No supportedSchemes");
asyncSetupDrmError.set(true);
mOnDrmInfoCalled.signal();
// we won't call prepareDrm anymore but need to get passed the wait
mOnDrmPreparedCalled.signal();
return;
}
// setting up with the first supported UUID
// instead of supportedSchemes[0] in GTS
UUID drmScheme = CLEARKEY_SCHEME_UUID;
Log.d(TAG, "preparePlayerAndDrm_V3: onDrmInfo: selected " + drmScheme);
try {
Log.v(TAG, "preparePlayerAndDrm_V3: onDrmInfo: calling prepareDrm");
mp.prepareDrm(drmScheme);
Log.v(TAG, "preparePlayerAndDrm_V3: onDrmInfo: called prepareDrm");
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "preparePlayerAndDrm_V3: onDrmInfo: prepareDrm exception " + e);
asyncSetupDrmError.set(true);
mOnDrmInfoCalled.signal();
// need to get passed the wait
mOnDrmPreparedCalled.signal();
return;
}
mOnDrmInfoCalled.signal();
Log.v(TAG, "preparePlayerAndDrm_V3: onDrmInfo done!");
}
@Override
public void onDrmPrepared(MediaPlayer2 mp, MediaItem item, int status) {
Log.v(TAG, "preparePlayerAndDrm_V3: onDrmPrepared status: " + status);
assertTrue("preparePlayerAndDrm_V3: onDrmPrepared did not succeed",
status == MediaPlayer2.PREPARE_DRM_STATUS_SUCCESS);
DrmInfo drmInfo = mPlayer.getDrmInfo();
// in the callback (async mode) so handling exceptions here
try {
setupDrm(drmInfo, false /* prepareDrm */, true /* synchronousNetworking */,
MediaDrm.KEY_TYPE_STREAMING);
} catch (Exception e) {
Log.v(TAG, "preparePlayerAndDrm_V3: setupDrm EXCEPTION " + e);
asyncSetupDrmError.set(true);
}
mOnDrmPreparedCalled.signal();
Log.v(TAG, "preparePlayerAndDrm_V3: onDrmPrepared done!");
}
});
Log.v(TAG, "preparePlayerAndDrm_V3: calling prepare()");
mPlayer.prepare();
// Waiting till the player is prepared
mOnPreparedCalled.waitForSignal();
// Unlike v3, onDrmPrepared is not synced to onPrepared b/c of its own thread handler
mOnDrmPreparedCalled.waitForSignal();
// to handle setupDrm error (async) in the main thread rather than the callback
if (asyncSetupDrmError.get()) {
fail("preparePlayerAndDrm_V3: setupDrm");
}
}
private void playLoadedModularDrmVideo_V4_offlineKey(final Uri file, final Integer width,
final Integer height, int playTime) throws Exception {
final float volume = 0.5f;
mAudioOnly = (width == 0);
SurfaceHolder surfaceHolder = mActivity.getSurfaceHolder();
Log.v(TAG, "playLoadedModularDrmVideo_V4_offlineKey: setSurface " + surfaceHolder);
mPlayer.setSurface(surfaceHolder.getSurface());
surfaceHolder.setKeepScreenOn(true);
mCallStatus = MediaPlayer2.CALL_STATUS_NO_ERROR;
DrmInfo drmInfo = null;
for (int round = 0; round < 2; round++) {
boolean keyRequestRound = (round == 0);
boolean restoreRound = (round == 1);
Log.v(TAG, "playLoadedVideo: round " + round);
try {
mPlayer.setEventCallback(mExecutor, mECb);
Log.v(TAG, "playLoadedVideo: setMediaItem()");
mPlayer.setMediaItem(new UriMediaItem.Builder(file).build());
Log.v(TAG, "playLoadedVideo: prepare()");
mPlayer.prepare();
mOnPreparedCalled.waitForSignal();
// but preparing the DRM every time with proper key request type
drmInfo = mPlayer.getDrmInfo();
if (drmInfo != null) {
if (keyRequestRound) {
// asking for offline keys
setupDrm(drmInfo, true /* prepareDrm */, true /* synchronousNetworking */,
MediaDrm.KEY_TYPE_OFFLINE);
} else if (restoreRound) {
setupDrmRestore(drmInfo, true /* prepareDrm */);
} else {
fail("preparePlayer: unexpected round " + round);
}
Log.v(TAG, "preparePlayer: setupDrm done!");
}
} catch (IOException e) {
e.printStackTrace();
throw new PrepareFailedException();
}
Log.v(TAG, "playLoadedVideo: play()");
mPlayer.play();
if (!mAudioOnly) {
mOnVideoSizeChangedCalled.waitForSignal();
}
mPlayer.setPlayerVolume(volume);
// waiting to complete
if (playTime == 0) {
Log.v(TAG, "playLoadedVideo: waiting for playback completion");
mOnPlaybackCompleted.waitForSignal();
} else {
Log.v(TAG, "playLoadedVideo: waiting while playing for " + playTime);
mOnPlaybackCompleted.waitForSignal(playTime);
}
try {
if (drmInfo != null) {
if (restoreRound) {
// releasing the offline key
setupDrm(null /* drmInfo */, false /* prepareDrm */,
true /* synchronousNetworking */, MediaDrm.KEY_TYPE_RELEASE);
Log.v(TAG, "playLoadedVideo: released offline keys");
}
Log.v(TAG, "playLoadedVideo: releaseDrm");
mPlayer.releaseDrm();
}
} catch (Exception e) {
e.printStackTrace();
throw new PrepareFailedException();
}
if (keyRequestRound) {
mOnPreparedCalled.reset();
mOnVideoSizeChangedCalled.reset();
mOnPlaybackCompleted.reset();
final int sleepBetweenRounds = 1000;
Thread.sleep(sleepBetweenRounds);
Log.v(TAG, "playLoadedVideo: reset");
mPlayer.reset();
}
} // for
}
// Converts a BMFF PSSH initData to a raw cenc initData
protected byte[] makeCencPSSH(UUID uuid, byte[] bmffPsshData) {
byte[] pssh_header = new byte[] { (byte) 'p', (byte) 's', (byte) 's', (byte) 'h' };
byte[] pssh_version = new byte[] { 1, 0, 0, 0 };
int boxSizeByteCount = 4;
int uuidByteCount = 16;
int dataSizeByteCount = 4;
// Per "W3C cenc Initialization Data Format" document:
// box size + 'pssh' + version + uuid + payload + size of data
int boxSize = boxSizeByteCount + pssh_header.length + pssh_version.length
+ uuidByteCount + bmffPsshData.length + dataSizeByteCount;
int dataSize = 0;
// the default write is big-endian, i.e., network byte order
ByteBuffer rawPssh = ByteBuffer.allocate(boxSize);
rawPssh.putInt(boxSize);
rawPssh.put(pssh_header);
rawPssh.put(pssh_version);
rawPssh.putLong(uuid.getMostSignificantBits());
rawPssh.putLong(uuid.getLeastSignificantBits());
rawPssh.put(bmffPsshData);
rawPssh.putInt(dataSize);
return rawPssh.array();
}
/*
* Sets up the DRM for the first DRM scheme from the supported list.
*
* @param drmInfo DRM info of the source
* @param prepareDrm whether prepareDrm should be called
* @param synchronousNetworking whether the network operation of key request/response will
* be performed synchronously
*/
private void setupDrm(DrmInfo drmInfo, boolean prepareDrm, boolean synchronousNetworking,
int keyType) throws Exception {
Log.d(TAG, "setupDrm: drmInfo: " + drmInfo + " prepareDrm: " + prepareDrm
+ " synchronousNetworking: " + synchronousNetworking);
try {
byte[] initData = null;
String mime = null;
String keyTypeStr = "Unexpected";
switch (keyType) {
case MediaDrm.KEY_TYPE_STREAMING:
case MediaDrm.KEY_TYPE_OFFLINE:
// DRM preparation
List<UUID> supportedSchemes = drmInfo.getSupportedSchemes();
if (supportedSchemes.isEmpty()) {
fail("setupDrm: No supportedSchemes");
}
// instead of supportedSchemes[0] in GTS
UUID drmScheme = CLEARKEY_SCHEME_UUID;
Log.d(TAG, "setupDrm: selected " + drmScheme);
if (prepareDrm) {
final Monitor drmPrepared = new Monitor();
mPlayer.setDrmEventCallback(mExecutor, new MediaPlayer2.DrmEventCallback() {
@Override
public void onDrmPrepared(
MediaPlayer2 mp, MediaItem item, int status) {
drmPrepared.signal();
}
});
mPlayer.prepareDrm(drmScheme);
drmPrepared.waitForSignal();
}
byte[] psshData = drmInfo.getPssh().get(drmScheme);
// diverging from GTS
if (psshData == null) {
initData = mClearKeyPssh;
Log.d(TAG, "setupDrm: CLEARKEY scheme not found in PSSH."
+ " Using default data.");
} else {
// Can skip conversion if ClearKey adds support for BMFF initData b/64863112
initData = makeCencPSSH(CLEARKEY_SCHEME_UUID, psshData);
}
Log.d(TAG, "setupDrm: initData[" + drmScheme + "]: "
+ Arrays.toString(initData));
// diverging from GTS
mime = "cenc";
keyTypeStr = (keyType == MediaDrm.KEY_TYPE_STREAMING)
? "KEY_TYPE_STREAMING" : "KEY_TYPE_OFFLINE";
break;
case MediaDrm.KEY_TYPE_RELEASE:
if (mKeySetId == null) {
fail("setupDrm: KEY_TYPE_RELEASE requires a valid keySetId.");
}
keyTypeStr = "KEY_TYPE_RELEASE";
break;
default:
fail("setupDrm: Unexpected keyType " + keyType);
}
final MediaDrm.KeyRequest request = mPlayer.getDrmKeyRequest(
(keyType == MediaDrm.KEY_TYPE_RELEASE) ? mKeySetId : null,
initData,
mime,
keyType,
null /* optionalKeyRequestParameters */
);
Log.d(TAG, "setupDrm: mPlayer.getDrmKeyRequest(" + keyTypeStr
+ ") request -> " + request);
// diverging from GTS
byte[][] clearKeys = new byte[][] { CLEAR_KEY_CENC };
byte[] response = createKeysResponse(request, clearKeys);
// null is returned when the response is for a streaming or release request.
byte[] keySetId = mPlayer.provideDrmKeyResponse(
(keyType == MediaDrm.KEY_TYPE_RELEASE) ? mKeySetId : null,
response);
Log.d(TAG, "setupDrm: provideDrmKeyResponse -> " + Arrays.toString(keySetId));
// storing offline key for a later restore
mKeySetId = (keyType == MediaDrm.KEY_TYPE_OFFLINE) ? keySetId : null;
} catch (MediaPlayer2.NoDrmSchemeException e) {
Log.d(TAG, "setupDrm: NoDrmSchemeException");
e.printStackTrace();
throw e;
} catch (Exception e) {
Log.d(TAG, "setupDrm: Exception " + e);
e.printStackTrace();
throw e;
}
} // setupDrm
private void setupDrmRestore(DrmInfo drmInfo, boolean prepareDrm) throws Exception {
Log.d(TAG, "setupDrmRestore: drmInfo: " + drmInfo + " prepareDrm: " + prepareDrm);
try {
if (prepareDrm) {
// DRM preparation
List<UUID> supportedSchemes = drmInfo.getSupportedSchemes();
if (supportedSchemes.isEmpty()) {
fail("setupDrmRestore: No supportedSchemes");
}
// instead of supportedSchemes[0] in GTS
UUID drmScheme = CLEARKEY_SCHEME_UUID;
Log.d(TAG, "setupDrmRestore: selected " + drmScheme);
mPlayer.prepareDrm(drmScheme);
}
if (mKeySetId == null) {
fail("setupDrmRestore: Offline key has not been setup.");
}
mPlayer.restoreDrmKeys(mKeySetId);
} catch (MediaPlayer2.NoDrmSchemeException e) {
Log.v(TAG, "setupDrmRestore: NoDrmSchemeException");
e.printStackTrace();
throw e;
} catch (Exception e) {
Log.v(TAG, "setupDrmRestore: Exception " + e);
e.printStackTrace();
throw e;
}
} // setupDrmRestore
//////////////////////////////////////////////////////////////////////////////////////////////
// Diverging from GTS
// Clearkey helpers
/**
* Convert a hex string into byte array.
*/
private static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i + 1), 16));
}
return data;
}
/**
* Extracts key ids from the pssh blob returned by getDrmKeyRequest() and
* places it in keyIds.
* keyRequestBlob format (section 5.1.3.1):
* https://dvcs.w3.org/hg/html-media/raw-file/default/encrypted-media/encrypted-media.html
*
* @return size of keyIds vector that contains the key ids, 0 for error
*/
private int getKeyIds(byte[] keyRequestBlob, Vector<String> keyIds) {
if (0 == keyRequestBlob.length || keyIds == null) {
Log.e(TAG, "getKeyIds: Empty keyRequestBlob or null keyIds.");
return 0;
}
String jsonLicenseRequest = new String(keyRequestBlob);
keyIds.clear();
try {
JSONObject license = new JSONObject(jsonLicenseRequest);
Log.v(TAG, "getKeyIds: license: " + license);
final JSONArray ids = license.getJSONArray("kids");
Log.v(TAG, "getKeyIds: ids: " + ids);
for (int i = 0; i < ids.length(); ++i) {
keyIds.add(ids.getString(i));
}
} catch (JSONException e) {
Log.e(TAG, "Invalid JSON license = " + jsonLicenseRequest);
return 0;
}
return keyIds.size();
}
/**
* Creates the JSON Web Key string.
*
* @return JSON Web Key string.
*/
private String createJsonWebKeySet(Vector<String> keyIds, Vector<String> keys) {
String jwkSet = "{\"keys\":[";
for (int i = 0; i < keyIds.size(); ++i) {
String id = new String(keyIds.get(i).getBytes(Charset.forName("UTF-8")));
String key = new String(keys.get(i).getBytes(Charset.forName("UTF-8")));
jwkSet += "{\"kty\":\"oct\",\"kid\":\"" + id + "\",\"k\":\"" + key + "\"}";
}
jwkSet += "]}";
return jwkSet;
}
/**
* Retrieves clear key ids from KeyRequest and creates the response in place.
*/
private byte[] createKeysResponse(MediaDrm.KeyRequest keyRequest, byte[][] clearKeys) {
Vector<String> keyIds = new Vector<String>();
if (0 == getKeyIds(keyRequest.getData(), keyIds)) {
Log.e(TAG, "No key ids found in initData");
return null;
}
if (clearKeys.length != keyIds.size()) {
Log.e(TAG, "Mismatch number of key ids and keys: ids="
+ keyIds.size() + ", keys=" + clearKeys.length);
return null;
}
// Base64 encodes clearkeys. Keys are known to the application.
Vector<String> keys = new Vector<String>();
for (int i = 0; i < clearKeys.length; ++i) {
String clearKey = Base64.encodeToString(clearKeys[i],
Base64.NO_PADDING | Base64.NO_WRAP);
keys.add(clearKey);
}
String jwkSet = createJsonWebKeySet(keyIds, keys);
byte[] jsonResponse = jwkSet.getBytes(Charset.forName("UTF-8"));
return jsonResponse;
}
//////////////////////////////////////////////////////////////////////////////////////////////
// Playback/download helpers
private static class MediaDownloadManager {
private static final String TAG = "MediaDownloadManager";
private final Context mContext;
private final DownloadManager mDownloadManager;
MediaDownloadManager(Context context) {
mContext = context;
mDownloadManager =
(DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
}
public long downloadFileWithRetries(Uri uri, Uri file, long timeout, int retries)
throws Exception {
long id = -1;
for (int i = 0; i < retries; i++) {
try {
id = downloadFile(uri, file, timeout);
if (id != -1) {
break;
}
} catch (Exception e) {
removeFile(id);
Log.w(TAG, "Download failed " + i + " times ");
}
}
return id;
}
public long downloadFile(Uri uri, Uri file, long timeout) throws Exception {
Log.i(TAG, "uri:" + uri + " file:" + file + " wait:" + timeout + " Secs");
final DownloadReceiver receiver = new DownloadReceiver();
long id = -1;
try {
IntentFilter intentFilter =
new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
if (Build.VERSION.SDK_INT < 33) {
mContext.registerReceiver(receiver, intentFilter);
} else {
mContext.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED);
}
Request request = new Request(uri);
request.setDestinationUri(file);
id = mDownloadManager.enqueue(request);
Log.i(TAG, "enqueue:" + id);
receiver.waitForDownloadComplete(timeout, id);
} finally {
mContext.unregisterReceiver(receiver);
}
return id;
}
public void removeFile(long id) {
Log.i(TAG, "removeFile:" + id);
mDownloadManager.remove(id);
}
public Uri getUriForDownloadedFile(long id) {
return mDownloadManager.getUriForDownloadedFile(id);
}
private final class DownloadReceiver extends BroadcastReceiver {
private HashSet<Long> mCompleteIds = new HashSet<>();
@Override
public void onReceive(Context context, Intent intent) {
synchronized (mCompleteIds) {
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
mCompleteIds.add(
intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1));
mCompleteIds.notifyAll();
}
}
}
private boolean isCompleteLocked(long... ids) {
for (long id : ids) {
if (!mCompleteIds.contains(id)) {
return false;
}
}
return true;
}
public void waitForDownloadComplete(long timeoutSecs, long... waitForIds)
throws InterruptedException {
if (waitForIds.length == 0) {
throw new IllegalArgumentException("Missing IDs to wait for");
}
final long startTime = SystemClock.elapsedRealtime();
do {
synchronized (mCompleteIds) {
mCompleteIds.wait(1000);
if (isCompleteLocked(waitForIds)) {
return;
}
}
} while ((SystemClock.elapsedRealtime() - startTime) < timeoutSecs * 1000);
throw new InterruptedException(
"Timeout waiting for IDs " + Arrays.toString(waitForIds)
+ "; received " + mCompleteIds.toString()
+ ". Make sure you have WiFi or some other connectivity for this test.");
}
}
} // MediaDownloadManager
}