Adding playback of PRM encrypted content
In Android Studio, add your Operator Vault to your application build project as a project resource.
Make sure
READ_PHONE_STATE
permission is declared in the application’sAndroidManifest.xml
.XML<uses-permission android:name="android.permission.READ_PHONE_STATE" />
Make sure the SDK is loaded in the application’s initial activity.
JAVAOTVSDK.load(this);
Before initialising the PAK, the
READ_PHONE_STATE
permission must be requested at runtime when all of the following conditions are true:- The application will run on Android P devices.
- The application
targetSdkVersion
is 28 (Android P) or higher. - The Operator Vault file
EnforceSingleDeviceUniqueIDPerDeviceFlag
is set totrue
.
The following code will, if necessary, show a system dialogue box requesting approval from the user. If permission has not already been granted, the user’s approval comes back asynchronously in the
onRequestPermissionsResult()
callback (which is anActivity
override).JAVAif (checkReadPhoneStatePermission()) { // Permission granted. Can continue. } else { // Permission not granted yet. Need to wait for user's approval. } public boolean checkReadPhoneStatePermission() { boolean hasPermission = (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED); if (!hasPermission) { requestReadPhoneStatePermission(); } return hasPermission; } @TargetApi(23) public void requestReadPhoneStatePermission() { if (shouldShowRequestPermissionRationale(Manifest.permission.READ_PHONE_STATE)) { // Show our own request permission dialog before popping the system one. AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); dialogBuilder.setTitle("External permission required for Read Phone State"); dialogBuilder.setPositiveButton("Ok", (dialog, which) -> requestPermissions(new String[]{Manifest.permission.READ_PHONE_STATE}, 1)) .show(); } else { // Show system dialog to request permission requestPermissions(new String[]{Manifest.permission.READ_PHONE_STATE}, 1); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { // Dialogue results callback if (requestCode != 1) { // Not coming from our request, tagged as 1 return; } if (grantResults.length == 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED) { // Permission refused/not granted by user } else { // Permission granted by user } }
Prepare PRM by instantiating
OTVPRMManager
and initialising the PAK library with the Operator Vault. Convert the resource into a byte array.JAVA// Read OpVault resource as byte array byte[] opvault = null; try (InputStream opvaultStream = getResources().openRawResource(R.raw.opvault)) { opvault = new byte[opvaultStream.available()]; opvaultStream.read(opvault); } catch (IOException e) { OTVLog.e(TAG, "Failed to read opvault", e); return; } // Create an OTVPRMManager and prepare it with context and opvault OTVPRMManager prmManager = new OTVPRMManager(); if (!prmManager.preparePAK(this, opvault)) { OTVLog.e(TAG, "Failed to prepare PAK"); return; }
As PRM preparation is typically a one-off exercise, this can also be done in the initial activity.
The application needs to create a callback class implementing
OTVPRMNonSilentCallback
. Using theOTVSSPPRMNonSilentCallback
, create the callback with the data specific to the licence server. Create a listener; in the example below, a semaphore is implemented for later use in a wait context.JAVAprivate Semaphore mPakResponseSemaphore; private boolean mIsPrmInitialised = false; private OTVPAKReadyListener mPAKReadyListener = new OTVPAKReadyListener() { @Override public void onReady() { OTVLog.d(TAG, "PRM successfully initialized"); if (mPakResponseSemaphore != null) { mPakResponseSemaphore.release(); } } @Override public void onError() { OTVLog.w(TAG, "Failed to initialize PRM"); if (mPakResponseSemaphore != null) { mPakResponseSemaphore.release(); } } };
Initialise with server data. For the
OTVSSPPRMNonSilentCallback
implementation ofOTVPRMNonSilentCallback
, you also need to provide request properties, which are specific HTTP headers for initialisation and key requests.JAVAString PRM_SERVER_URL = "https://path/to/licence-server/"; OTVSSPPRMNonSilentCallback prmCallback = new OTVSSPPRMNonSilentCallback(PRM_SERVER_URL); // Setting request properties specific for SSP prmCallback.setKeyRequestProperty("Accept", "application/json"); prmCallback.setKeyRequestProperty("Content-Type", "application/json"); prmCallback.setKeyRequestProperty("nv-portal-id", "SSP AuthZ"); // Server private data is a secret between the PRM client and licence server String PRM_SERVER_PRIVATE_DATA = "private-data-specific-to-server"; prmCallback.setInitializationClientProtectedPrivateData(PRM_SERVER_PRIVATE_DATA); mPRMManager.initializePrmWithCallbacks(prmCallback, mPAKReadyListener); // initializePrmWithCallbacks is asynchronous and the listener will get onReady() // when the initialisation is complete. However, if PRM was already initialised, onReady() // may not be called. Therefore we check below if initialisation has already taken place. mIsPrmInitialised = mPrmManager.isPrmInitialized();
Once everything is set, wait for the initialisation to complete.
JAVA// At this point, PRM may already be initialised, so no need to wait. if (!mIsPrmInitialised) { // Wait for onReady() or onError() from manager. mPakResponseSemaphore = new Semaphore(0); boolean waitResult = false; try { waitResult = mPakResponseSemaphore.tryAcquire(5, TimeUnit.SECONDS); } catch (InterruptedException e) { OTVLog.e(TAG, "PAK initialization thread interrupted", e); Thread.currentThread().interrupt(); } if (!waitResult) { OTVLog.e(TAG, "Timed out waiting for PAK initialization response"); } } mIsPrmInitialised = mPrmManager.isPrmInitialized(); OTVLog.d(TAG, "mIsPrmInitialised = " + mIsPrmInitialised);
To detect when and why PRM key retrieval fails, it is useful to listen to session events. Within the fragment/activity, create an internal class, implementing the
OTVPRMSessionEventListener
interface, implementing the two event methods.JAVAprivate OTVPRMSessionEventListener mPrmSessionEventListener = new OTVPRMSessionEventListener() { @Override public void onPrmKeysLoadedSuccess() { // Do something - Prm key loaded } @Override public void onPrmKeysLoadedFail(OTVPRMException error) { int erroCode = error.errorCode(); switch (erroCode) { case OTVPRMException.ERROR_FETCH_LICENSE: // Do something - ERROR_FETCH_LICENSE break; case OTVPRMException.ERROR_LICENSE_EXPIRED: // Do something - ERROR_LICENSE_EXPIRED break; case OTVPRMException.ERROR_INVALID_ENTITLEMENT: // Do something - ERROR_INVALID_ENTITLEMENT break; default: // Shouldn't get here break; } };
The above listener should be added to the
OTVPRMManager
during the initialisation.JAVAmPRMManager.addPRMSessionEventListener(mPrmSessionEventListener);
Before starting playback, the stream token must be provided to the callback instance.
JAVA// The stream token is information specific for fetching the licence for the stream. // Depending on the licence server and stream signalisation, // it may also contain the PRM Content ID. String STREAM_TOKEN = "private-data-specific-to-stream" prmCallback.setLicenseRequestClientProtectedPrivateData(STREAM_TOKEN);
Playback operations can now proceed the same way as with clear data.
JAVAOTVVideoView.setVideoPath(); OTVVideoView.start();
In most cases, the PAK can remain active throughout the application’s lifecycle. However, if PAK needs to be released, the following should be called:
JAVAif (mPRMManager != null) { mPRMManager.releasePAK(false); }
Setting the
releasePAK()
parameter totrue
will also erase the PAK database, which includes licences for offline playback. Unless the licence server is changed, there is no need to clear the database.