package com.kaltura.playersdk.drm;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.DeniedByServerException;
import android.media.MediaCryptoException;
import android.media.MediaDrm;
import android.media.MediaDrmException;
import android.media.NotProvisionedException;
import android.media.UnsupportedSchemeException;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Base64;
import android.util.Log;
import com.google.android.libraries.mediaframework.exoplayerextensions.ExoplayerUtil;
import com.kaltura.playersdk.LocalAssetsManager;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import static com.google.android.exoplayer.drm.StreamingDrmSessionManager.WIDEVINE_UUID;
/**
* Created by noamt on 19/04/2016.
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public class WidevineModularAdapter extends DrmAdapter {
private static final String TAG = "WidevineModularAdapter";
private final OfflineKeySetStorage mStore;
public static boolean isSupported() {
// Make sure Widevine is supported.
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && MediaDrm.isCryptoSchemeSupported(WIDEVINE_UUID);
}
WidevineModularAdapter(Context context) {
mStore = OfflineDrmManager.getStorage(context);
}
private byte[] httpPost(@NonNull String licenseUri, byte[] data) throws IOException {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/octet-stream");
return ExoplayerUtil.executePost(licenseUri, data, headers);
}
@Override
public boolean registerAsset(@NonNull String localPath, String licenseUri, @Nullable LocalAssetsManager.AssetRegistrationListener listener) {
try {
boolean result = registerAsset(localPath, licenseUri);
if (listener != null) {
listener.onRegistered(localPath);
}
return result;
} catch (RegisterException e) {
if (listener != null) {
listener.onFailed(localPath, e);
}
return false;
}
}
private class RegisterException extends Exception {
RegisterException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
}
}
private class NoWidevinePSSHException extends RegisterException {
NoWidevinePSSHException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
}
}
private SimpleDashParser parseDash(@NonNull String localPath) throws RegisterException {
SimpleDashParser dashParser;
try {
dashParser = new SimpleDashParser().parse(localPath);
if (dashParser.format == null) {
throw new RegisterException("Unknown format", null);
}
if (dashParser.hasContentProtection && dashParser.widevineInitData == null) {
throw new NoWidevinePSSHException("No Widevine PSSH in media", null);
}
} catch (IOException e) {
throw new RegisterException("Can't parse local dash", e);
}
return dashParser;
}
private boolean registerAsset(@NonNull String localPath, String licenseUri) throws RegisterException {
SimpleDashParser dash = parseDash(localPath);
if (!dash.hasContentProtection) {
// Not protected -- nothing to do.
return true;
}
String mimeType = dash.format.mimeType;
byte[] initData = dash.widevineInitData;
MediaDrmSession session;
MediaDrm mediaDrm = createMediaDrm();
try {
session = MediaDrmSession.open(mediaDrm);
} catch (MediaDrmException e) {
throw new RegisterException("Can't open session", e);
}
// Get keyRequest
MediaDrm.KeyRequest keyRequest = session.getOfflineKeyRequest(initData, mimeType);
Log.d(TAG, "registerAsset: init data (b64): " + Base64.encodeToString(initData, Base64.NO_WRAP));
byte[] data = keyRequest.getData();
// Send request to server
byte[] keyResponse;
try {
Log.d(TAG, "registerAsset: request data (b64): " + Base64.encodeToString(data, Base64.NO_WRAP));
keyResponse = httpPost(licenseUri, data);
Log.d(TAG, "registerAsset: response data (b64): " + Base64.encodeToString(keyResponse, Base64.NO_WRAP));
} catch (IOException e) {
throw new RegisterException("Can't send key request for registration", e);
}
// Provide keyResponse
try {
byte[] offlineKeyId = session.provideKeyResponse(keyResponse);
mStore.storeKeySetId(initData, offlineKeyId);
} catch (DeniedByServerException e) {
throw new RegisterException("Request denied by server", e);
}
session.close();
return true;
}
private boolean unregisterAsset(String localPath) throws RegisterException {
SimpleDashParser dash = parseDash(localPath);
byte[] keySetId;
try {
keySetId = mStore.loadKeySetId(dash.widevineInitData);
} catch (FileNotFoundException e) {
throw new RegisterException("Can't unregister -- keySetId not found", e);
}
MediaDrm mediaDrm = createMediaDrm();
MediaDrm.KeyRequest releaseRequest;
try {
releaseRequest = mediaDrm.getKeyRequest(keySetId, null, null, MediaDrm.KEY_TYPE_RELEASE, null);
} catch (NotProvisionedException e) {
throw new WidevineNotSupportedException(e);
}
Log.d(TAG, "releaseRequest:" + Base64.encodeToString(releaseRequest.getData(), Base64.NO_WRAP));
mStore.removeKeySetId(dash.widevineInitData);
return true;
}
@Override
public boolean unregisterAsset(@NonNull String localPath, LocalAssetsManager.AssetRemovalListener listener) {
// TODO
try {
unregisterAsset(localPath);
return true;
} catch (RegisterException e) {
Log.e(TAG, "Failed to unregister", e);
return false;
} finally {
if (listener != null) {
listener.onRemoved(localPath);
}
}
}
@NonNull
private MediaDrm createMediaDrm() throws RegisterException {
MediaDrm mediaDrm;
try {
mediaDrm = new MediaDrm(WIDEVINE_UUID);
} catch (UnsupportedSchemeException e) {
throw new WidevineNotSupportedException(e);
}
return mediaDrm;
}
@Override
public boolean refreshAsset(@NonNull String localPath, String licenseUri, @Nullable LocalAssetsManager.AssetRegistrationListener listener) {
// TODO -- verify that we just need to register again
return registerAsset(localPath, licenseUri, listener);
}
@Override
public boolean checkAssetStatus(@NonNull String localPath, @Nullable LocalAssetsManager.AssetStatusListener listener) {
try {
Map<String, String> assetStatus = checkAssetStatus(localPath);
if (assetStatus != null) {
long licenseDurationRemaining = 0;
long playbackDurationRemaining = 0;
try {
licenseDurationRemaining = Long.parseLong(assetStatus.get("LicenseDurationRemaining"));
playbackDurationRemaining = Long.parseLong(assetStatus.get("PlaybackDurationRemaining"));
} catch (NumberFormatException e) {
Log.e(TAG, "Invalid integers in KeyStatus: " + assetStatus);
}
if (listener != null) {
listener.onStatus(localPath, licenseDurationRemaining, playbackDurationRemaining);
}
}
} catch (NoWidevinePSSHException e) {
// Not a Widevine file
if (listener != null) {
listener.onStatus(localPath, -1, -1);
}
return false;
} catch (RegisterException e) {
if (listener != null) {
listener.onStatus(localPath, 0, 0);
}
return false;
}
return true;
}
private Map<String, String> checkAssetStatus(@NonNull String localPath) throws RegisterException {
SimpleDashParser dashParser;
try {
dashParser = new SimpleDashParser().parse(localPath);
} catch (IOException e) {
throw new RegisterException("Can't parse dash", e);
}
if (dashParser.widevineInitData == null) {
throw new NoWidevinePSSHException("No Widevine PSSH in media", null);
}
MediaDrm mediaDrm = createMediaDrm();
MediaDrmSession session;
try {
session = OfflineDrmManager.openSessionWithKeys(mediaDrm, mStore, dashParser.widevineInitData);
} catch (MediaDrmException | MediaCryptoException | FileNotFoundException e) {
throw new RegisterException("Can't open session with keys", e);
}
Map<String, String> keyStatus = session.queryKeyStatus();
session.close();
mediaDrm.release();
return keyStatus;
}
@Override
public DRMScheme getScheme() {
return DRMScheme.WidevineCENC;
}
}