blob: 94e75eb781796f5e0a73e5d28a5d83f4f6d32e9d [file] [log] [blame]
/*
* Copyright (C) 2017 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 android.media.cts;
import android.app.DownloadManager;
import android.app.DownloadManager.Request;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.media.MediaDrm;
import android.media.MediaPlayer;
import android.media.MediaPlayer.DrmInfo;
import android.media.ResourceBusyException;
import android.media.UnsupportedSchemeException;
import android.media.cts.MediaPlayerTestBase.Monitor;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.PersistableBundle;
import android.os.SystemClock;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Base64;
import android.util.Log;
import com.android.compatibility.common.util.MediaUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpCookie;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.Vector;
import java.util.logging.Logger;
import java.util.concurrent.atomic.AtomicBoolean;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Base class for tests which use MediaPlayer to play audio or video.
*/
public class MediaPlayerDrmTestBase extends ActivityInstrumentationTestCase2<MediaStubActivity> {
private static final Logger LOG = Logger.getLogger(MediaPlayerTestBase.class.getName());
protected static final int STREAM_RETRIES = 3;
protected Monitor mOnVideoSizeChangedCalled = new Monitor();
protected Monitor mOnErrorCalled = new Monitor();
protected Context mContext;
protected Resources mResources;
protected MediaPlayer mMediaPlayer = null;
protected MediaStubActivity mActivity;
public MediaPlayerDrmTestBase() {
super(MediaStubActivity.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
mActivity = getActivity();
getInstrumentation().waitForIdleSync();
try {
runTestOnUiThread(new Runnable() {
public void run() {
mMediaPlayer = new MediaPlayer();
}
});
} catch (Throwable e) {
e.printStackTrace();
fail();
}
mContext = getInstrumentation().getTargetContext();
mResources = mContext.getResources();
}
@Override
protected void tearDown() throws Exception {
if (mMediaPlayer != null) {
mMediaPlayer.release();
mMediaPlayer = null;
}
mActivity = null;
super.tearDown();
}
protected void setOnErrorListener() {
mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
mOnErrorCalled.signal();
return false;
}
});
}
private static class PrepareFailedException extends Exception {}
//////////////////////////////////////////////////////////////////////////////////////////
// Modular DRM
private static final String TAG = "MediaPlayerDrmTestBase";
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[] CLEARKEY_PSSH = 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,
V5_ASYNC_DRMPREPARED_TEST_WITH_HANDLER,
}
// 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 DOWNLOAD_TIMEOUT_SECONDS = 600;
Log.i(TAG, "Downloading file:" + path);
MediaDownloadManager mediaDownloadManager = new MediaDownloadManager(mContext);
final long id = mediaDownloadManager.downloadFileWithRetries(
uri, path, DOWNLOAD_TIMEOUT_SECONDS, 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");
mMediaPlayer.stop();
mMediaPlayer.reset();
}
}
assertTrue("Stream did not play successfully after all attempts (syncDrmSetup)",
playedSuccessfully);
}
/**
* Play a video which has already been loaded with setDataSource().
* The DRM setup is performed synchronously.
*
* @param file data source
* @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:
case V5_ASYNC_DRMPREPARED_TEST_WITH_HANDLER:
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 leftVolume = 0.5f;
final float rightVolume = 0.5f;
mAudioOnly = (width == 0);
try {
Log.v(TAG, "playLoadedVideo: setDataSource()");
mMediaPlayer.setDataSource(mContext, file);
} catch (IOException e) {
e.printStackTrace();
throw new PrepareFailedException();
}
mMediaPlayer.setDisplay(mActivity.getSurfaceHolder());
mMediaPlayer.setScreenOnWhilePlaying(true);
mMediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
@Override
public void onVideoSizeChanged(MediaPlayer mp, int w, int h) {
Log.v(TAG, "VideoSizeChanged" + " w:" + w + " h:" + h);
mOnVideoSizeChangedCalled.signal();
}
});
mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
fail("Media player had error " + what + " playing video");
return true;
}
});
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;
case V5_ASYNC_DRMPREPARED_TEST_WITH_HANDLER:
preparePlayerAndDrm_V5_asyncDrmSetupWithHandler();
break;
}
} catch (IOException e) {
e.printStackTrace();
throw new PrepareFailedException();
}
final Monitor playbackCompleted = new Monitor();
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
Log.v(TAG, "playLoadedVideo: onCompletion");
playbackCompleted.signal();
}
});
Log.v(TAG, "playLoadedVideo: start()");
mMediaPlayer.start();
if (!mAudioOnly) {
mOnVideoSizeChangedCalled.waitForSignal();
}
mMediaPlayer.setVolume(leftVolume, rightVolume);
// waiting to complete
if (playTime == 0) {
Log.v(TAG, "playLoadedVideo: waiting for playback completion");
playbackCompleted.waitForSignal();
} else {
Log.v(TAG, "playLoadedVideo: waiting while playing for " + playTime);
playbackCompleted.waitForSignal(playTime);
}
Log.v(TAG, "playLoadedVideo: stopping");
mMediaPlayer.stop();
Log.v(TAG, "playLoadedVideo: stopped");
try {
Log.v(TAG, "playLoadedVideo: releaseDrm");
mMediaPlayer.releaseDrm();
} catch (Exception e) {
e.printStackTrace();
throw new PrepareFailedException();
}
}
private void preparePlayerAndDrm_V0_syncDrmSetup() throws Exception {
Log.v(TAG, "preparePlayerAndDrm_V0: calling prepare()");
mMediaPlayer.prepare();
DrmInfo drmInfo = mMediaPlayer.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 {
Monitor onPreparedCalled = new Monitor();
final AtomicBoolean asyncSetupDrmError = new AtomicBoolean(false);
mMediaPlayer.setOnDrmInfoListener(new MediaPlayer.OnDrmInfoListener() {
@Override
public void onDrmInfo(MediaPlayer mp, 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);
}
Log.v(TAG, "preparePlayerAndDrm_V1: onDrmInfo done!");
}
});
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
Log.v(TAG, "preparePlayerAndDrm_V1: onPrepared");
onPreparedCalled.signal();
}
});
Log.v(TAG, "preparePlayerAndDrm_V1: calling prepareAsync()");
mMediaPlayer.prepareAsync();
// Waiting till the player is prepared
onPreparedCalled.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 {
mMediaPlayer.setOnDrmConfigHelper(new MediaPlayer.OnDrmConfigHelper() {
@Override
public void onDrmConfig(MediaPlayer mp) {
String WIDEVINE_SECURITY_LEVEL_3 = "L3";
String SECURITY_LEVEL_PROPERTY = "securityLevel";
try {
String level = mp.getDrmPropertyString(SECURITY_LEVEL_PROPERTY);
Log.v(TAG, "preparePlayerAndDrm_V2: getDrmPropertyString: " +
SECURITY_LEVEL_PROPERTY + " -> " + level);
mp.setDrmPropertyString(SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3);
level = mp.getDrmPropertyString(SECURITY_LEVEL_PROPERTY);
Log.v(TAG, "preparePlayerAndDrm_V2: getDrmPropertyString: " +
SECURITY_LEVEL_PROPERTY + " -> " + level);
} catch (MediaPlayer.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()");
mMediaPlayer.prepare();
DrmInfo drmInfo = mMediaPlayer.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 {
Monitor onPreparedCalled = new Monitor();
final AtomicBoolean asyncSetupDrmError = new AtomicBoolean(false);
mMediaPlayer.setOnDrmInfoListener(new MediaPlayer.OnDrmInfoListener() {
@Override
public void onDrmInfo(MediaPlayer mp, DrmInfo drmInfo) {
Log.v(TAG, "preparePlayerAndDrm_V3: onDrmInfo" + drmInfo);
// DRM preperation
UUID[] supportedSchemes = drmInfo.getSupportedSchemes();
if (supportedSchemes.length == 0) {
Log.e(TAG, "preparePlayerAndDrm_V3: onDrmInfo: No supportedSchemes");
asyncSetupDrmError.set(true);
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);
return;
}
Log.v(TAG, "preparePlayerAndDrm_V3: onDrmInfo done!");
}
});
mMediaPlayer.setOnDrmPreparedListener(new MediaPlayer.OnDrmPreparedListener() {
@Override
public void onDrmPrepared(MediaPlayer mp, int status) {
Log.v(TAG, "preparePlayerAndDrm_V3: onDrmPrepared status: " + status);
assertTrue("preparePlayerAndDrm_V3: onDrmPrepared did not succeed",
status == MediaPlayer.PREPARE_DRM_STATUS_SUCCESS);
DrmInfo drmInfo = mMediaPlayer.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);
}
Log.v(TAG, "preparePlayerAndDrm_V3: onDrmPrepared done!");
}
});
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
Log.v(TAG, "preparePlayerAndDrm_V3: onPrepared");
onPreparedCalled.signal();
Log.v(TAG, "preparePlayerAndDrm_V3: onPrepared done!");
}
});
Log.v(TAG, "preparePlayerAndDrm_V3: calling prepareAsync()");
mMediaPlayer.prepareAsync();
// Waiting till the player is prepared
onPreparedCalled.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 leftVolume = 0.5f;
final float rightVolume = 0.5f;
mAudioOnly = (width == 0);
Log.v(TAG, "playLoadedModularDrmVideo_V4_offlineKey: setDisplay " +
mActivity.getSurfaceHolder());
mMediaPlayer.setDisplay(mActivity.getSurfaceHolder());
mMediaPlayer.setScreenOnWhilePlaying(true);
mMediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
@Override
public void onVideoSizeChanged(MediaPlayer mp, int w, int h) {
Log.v(TAG, "VideoSizeChanged" + " w:" + w + " h:" + h);
mOnVideoSizeChangedCalled.signal();
}
});
mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
fail("Media player had error " + what + " playing video");
return true;
}
});
final Monitor playbackCompleted = new Monitor();
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
Log.v(TAG, "playLoadedVideo: onCompletion");
playbackCompleted.signal();
}
});
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 {
Log.v(TAG, "playLoadedVideo: setDataSource()");
mMediaPlayer.setDataSource(mContext, file);
Log.v(TAG, "playLoadedVideo: prepare()");
mMediaPlayer.prepare();
// but preparing the DRM every time with proper key request type
drmInfo = mMediaPlayer.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: start()");
mMediaPlayer.start();
if (!mAudioOnly) {
mOnVideoSizeChangedCalled.waitForSignal();
}
mMediaPlayer.setVolume(leftVolume, rightVolume);
// waiting to complete
if (playTime == 0) {
Log.v(TAG, "playLoadedVideo: waiting for playback completion");
playbackCompleted.waitForSignal();
} else {
Log.v(TAG, "playLoadedVideo: waiting while playing for " + playTime);
playbackCompleted.waitForSignal(playTime);
}
Log.v(TAG, "playLoadedVideo: stopping");
mMediaPlayer.stop();
Log.v(TAG, "playLoadedVideo: stopped");
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");
mMediaPlayer.releaseDrm();
}
} catch (Exception e) {
e.printStackTrace();
throw new PrepareFailedException();
}
if (keyRequestRound) {
playbackCompleted.reset();
final int SLEEP_BETWEEN_ROUNDS = 1000;
Thread.sleep(SLEEP_BETWEEN_ROUNDS);
Log.v(TAG, "playLoadedVideo: reset");
mMediaPlayer.reset();
}
} // for
}
private void preparePlayerAndDrm_V5_asyncDrmSetupWithHandler()
throws InterruptedException {
Monitor onPreparedCalled = new Monitor();
Monitor onDrmPreparedCalled = new Monitor();
final AtomicBoolean asyncSetupDrmError = new AtomicBoolean(false);
Log.v(TAG, "preparePlayerAndDrm_V5: started " + Thread.currentThread());
final HandlerThread handlerThread = new HandlerThread("ModDrmHandlerThread");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
mMediaPlayer.setOnDrmInfoListener(new MediaPlayer.OnDrmInfoListener() {
@Override
public void onDrmInfo(MediaPlayer mp, DrmInfo drmInfo) {
Log.v(TAG, "preparePlayerAndDrm_V5: onDrmInfo " + drmInfo +
" " + Thread.currentThread());
// DRM preperation
UUID[] supportedSchemes = drmInfo.getSupportedSchemes();
if (supportedSchemes.length == 0) {
Log.e(TAG, "preparePlayerAndDrm_V5: onDrmInfo: No supportedSchemes");
asyncSetupDrmError.set(true);
// we won't call prepareDrm anymore but need to get passed the wait
onDrmPreparedCalled.signal();
return;
}
// instead of supportedSchemes[0] in GTS
UUID drmScheme = CLEARKEY_SCHEME_UUID;
Log.d(TAG, "preparePlayerAndDrm_V5: onDrmInfo: selected " + drmScheme);
try {
Log.v(TAG, "preparePlayerAndDrm_V5: onDrmInfo: calling prepareDrm");
mp.prepareDrm(drmScheme);
Log.v(TAG, "preparePlayerAndDrm_V5: onDrmInfo: called prepareDrm");
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "preparePlayerAndDrm_V5: onDrmInfo: prepareDrm exception " + e);
asyncSetupDrmError.set(true);
// need to get passed the wait
onDrmPreparedCalled.signal();
return;
}
Log.v(TAG, "preparePlayerAndDrm_V5: onDrmInfo done!");
}
}, handler);
mMediaPlayer.setOnDrmPreparedListener(new MediaPlayer.OnDrmPreparedListener() {
@Override
public void onDrmPrepared(MediaPlayer mp, int status) {
Log.v(TAG, "preparePlayerAndDrm_V5: onDrmPrepared status: " + status +
" " + Thread.currentThread());
assertTrue("preparePlayerAndDrm_V5: onDrmPrepared did not succeed",
status == MediaPlayer.PREPARE_DRM_STATUS_SUCCESS);
DrmInfo drmInfo = mMediaPlayer.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_V5: setupDrm EXCEPTION " + e);
asyncSetupDrmError.set(true);
}
onDrmPreparedCalled.signal();
Log.v(TAG, "preparePlayerAndDrm_V5: onDrmPrepared done!");
}
}, handler);
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
Log.v(TAG, "preparePlayerAndDrm_V5: onPrepared " + Thread.currentThread());
onPreparedCalled.signal();
Log.v(TAG, "preparePlayerAndDrm_V5: onPrepared done!");
}
});
Log.v(TAG, "preparePlayerAndDrm_V5: calling prepareAsync()");
mMediaPlayer.prepareAsync();
// Waiting till the player is prepared
onPreparedCalled.waitForSignal();
// Unlike v3, onDrmPrepared is not synced to onPrepared b/c of its own thread handler
onDrmPreparedCalled.waitForSignal();
// to handle setupDrm error (async) in the main thread rather than the callback
if (asyncSetupDrmError.get()) {
fail("preparePlayerAndDrm_V5: setupDrm");
}
// stop the handler thread; callbacks are processed by now.
handlerThread.quit();
}
// 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
UUID[] supportedSchemes = drmInfo.getSupportedSchemes();
if (supportedSchemes.length == 0) {
fail("setupDrm: No supportedSchemes");
}
// instead of supportedSchemes[0] in GTS
UUID drmScheme = CLEARKEY_SCHEME_UUID;
Log.d(TAG, "setupDrm: selected " + drmScheme);
if (prepareDrm) {
mMediaPlayer.prepareDrm(drmScheme);
}
byte[] psshData = drmInfo.getPssh().get(drmScheme);
// diverging from GTS
if (psshData == null) {
initData = CLEARKEY_PSSH;
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 = mMediaPlayer.getKeyRequest(
(keyType == MediaDrm.KEY_TYPE_RELEASE) ? mKeySetId : null,
initData,
mime,
keyType,
null /* optionalKeyRequestParameters */
);
Log.d(TAG, "setupDrm: mMediaPlayer.getKeyRequest(" + 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 = mMediaPlayer.provideKeyResponse(
(keyType == MediaDrm.KEY_TYPE_RELEASE) ? mKeySetId : null,
response);
Log.d(TAG, "setupDrm: provideKeyResponse -> " + Arrays.toString(keySetId));
// storing offline key for a later restore
mKeySetId = (keyType == MediaDrm.KEY_TYPE_OFFLINE) ? keySetId : null;
} catch (MediaPlayer.NoDrmSchemeException e) {
Log.d(TAG, "setupDrm: NoDrmSchemeException");
e.printStackTrace();
throw e;
} catch (MediaPlayer.ProvisioningNetworkErrorException e) {
Log.d(TAG, "setupDrm: ProvisioningNetworkErrorException");
e.printStackTrace();
throw e;
} catch (MediaPlayer.ProvisioningServerErrorException e) {
Log.d(TAG, "setupDrm: ProvisioningServerErrorException");
e.printStackTrace();
throw e;
} catch (UnsupportedSchemeException e) {
Log.d(TAG, "setupDrm: UnsupportedSchemeException");
e.printStackTrace();
throw e;
} catch (ResourceBusyException e) {
Log.d(TAG, "setupDrm: ResourceBusyException");
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
UUID[] supportedSchemes = drmInfo.getSupportedSchemes();
if (supportedSchemes.length == 0) {
fail("setupDrmRestore: No supportedSchemes");
}
// instead of supportedSchemes[0] in GTS
UUID drmScheme = CLEARKEY_SCHEME_UUID;
Log.d(TAG, "setupDrmRestore: selected " + drmScheme);
mMediaPlayer.prepareDrm(drmScheme);
}
if (mKeySetId == null) {
fail("setupDrmRestore: Offline key has not been setup.");
}
mMediaPlayer.restoreKeys(mKeySetId);
} catch (MediaPlayer.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 getKeyRequest() 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;
public 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);
mContext.registerReceiver(receiver, intentFilter);
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<>();
public DownloadReceiver() {
}
@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
}