Skip to content

Instantly share code, notes, and snippets.

@seventhmoon
Last active December 1, 2016 09:33
Show Gist options
  • Select an option

  • Save seventhmoon/a036cf4231dada2739c4 to your computer and use it in GitHub Desktop.

Select an option

Save seventhmoon/a036cf4231dada2739c4 to your computer and use it in GitHub Desktop.

Revisions

  1. seventhmoon revised this gist Nov 30, 2015. 1 changed file with 0 additions and 12 deletions.
    12 changes: 0 additions & 12 deletions CachedDrmSessionManager.java
    Original file line number Diff line number Diff line change
    @@ -387,23 +387,16 @@ private void postKeyRequest() {
    String schemeDataString = Base64.encodeToString(schemeData, Base64.DEFAULT);

    SharedPreferences settings = mContext.getSharedPreferences("PREFS_NAME", 0);
    // SharedPreferences.Editor editor = settings.edit();

    String keySetId = settings.getString(schemeDataString, "");

    if (!"".equals(keySetId)) {
    mediaDrm.restoreKeys(sessionId, Base64.decode(keySetId, Base64.DEFAULT));
    // postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
    state = STATE_OPENED_WITH_KEYS;
    }else{
    postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();

    }





    } catch (NotProvisionedException e) {
    onKeysError(e);
    }
    @@ -538,15 +531,10 @@ public void handleMessage(Message msg) {
    try {
    switch (msg.what) {
    case MSG_PROVISION:
    // TODO: 26/11/2015 store and restore

    response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj);
    break;
    case MSG_KEYS:
    // TODO: 26/11/2015 store and restore

    response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj);
    Log.d(TAG, "callback.executeKeyRequest(uuid, (KeyRequest) msg.obj)");
    break;
    default:
    throw new RuntimeException();
  2. seventhmoon created this gist Nov 27, 2015.
    561 changes: 561 additions & 0 deletions CachedDrmSessionManager.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,561 @@
    /*
    * Copyright (C) 2014 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 com.google.android.exoplayer.drm;

    import android.annotation.SuppressLint;
    import android.annotation.TargetApi;
    import android.content.Context;
    import android.content.SharedPreferences;
    import android.media.DeniedByServerException;
    import android.media.MediaCrypto;
    import android.media.MediaDrm;
    import android.media.MediaDrm.KeyRequest;
    import android.media.MediaDrm.OnEventListener;
    import android.media.MediaDrm.ProvisionRequest;
    import android.media.NotProvisionedException;
    import android.media.UnsupportedSchemeException;
    import android.os.Handler;
    import android.os.HandlerThread;
    import android.os.Looper;
    import android.os.Message;
    import android.text.TextUtils;
    import android.util.Base64;
    import android.util.Log;

    import com.google.android.exoplayer.extractor.mp4.PsshAtomUtil;
    import com.google.android.exoplayer.util.Util;

    import java.math.BigInteger;
    import java.util.HashMap;
    import java.util.UUID;


    /**
    * A base class for {@link DrmSessionManager} implementations that support streaming playbacks
    * using {@link MediaDrm}.
    */
    @TargetApi(18)
    public class CachedDrmSessionManager implements DrmSessionManager {
    private static final String TAG = CachedDrmSessionManager.class.getSimpleName();

    /**
    * Interface definition for a callback to be notified of {@link CachedDrmSessionManager}
    * events.
    */
    public interface EventListener {

    /**
    * Invoked when a drm error occurs.
    *
    * @param e The corresponding exception.
    */
    void onDrmSessionManagerError(Exception e);

    }

    /**
    * UUID for the Widevine DRM scheme.
    */
    public static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);

    /**
    * UUID for the PlayReady DRM scheme.
    * <p>
    * Note that PlayReady is unsupported by most Android devices, with the exception of Android TV
    * devices, which do provide support.
    */
    public static final UUID PLAYREADY_UUID = new UUID(0x9A04F07998404286L, 0xAB92E65BE0885F95L);

    /**
    * The key to use when passing CustomData to a PlayReady instance in an optional parameter map.
    */
    public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData";

    private static final int MSG_PROVISION = 0;
    private static final int MSG_KEYS = 1;

    private final Handler eventHandler;
    private final EventListener eventListener;
    private final MediaDrm mediaDrm;
    private final HashMap<String, String> optionalKeyRequestParameters;

    /* package */ final MediaDrmHandler mediaDrmHandler;
    /* package */ final MediaDrmCallback callback;
    /* package */ final PostResponseHandler postResponseHandler;
    /* package */ final UUID uuid;

    private HandlerThread requestHandlerThread;
    private Handler postRequestHandler;

    private int openCount;
    private boolean provisioningInProgress;
    private int state;
    private MediaCrypto mediaCrypto;
    private Exception lastException;
    private String mimeType;
    private byte[] schemeData;
    private byte[] sessionId;
    private Context mContext;

    /**
    * Instantiates a new instance using the Widevine scheme.
    *
    * @param playbackLooper The looper associated with the media playback thread. Should usually be
    * obtained using {@link com.google.android.exoplayer.ExoPlayer#getPlaybackLooper()}.
    * @param callback Performs key and provisioning requests.
    * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
    * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
    * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
    * null if delivery of events is not required.
    * @param eventListener A listener of events. May be null if delivery of events is not required.
    * @throws UnsupportedDrmException If the specified DRM scheme is not supported.
    */
    public static CachedDrmSessionManager newWidevineInstance(Context context, Looper playbackLooper,
    MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters,
    Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException {
    return new CachedDrmSessionManager(context, WIDEVINE_UUID, playbackLooper, callback,
    optionalKeyRequestParameters, eventHandler, eventListener);
    }

    /**
    * Instantiates a new instance using the PlayReady scheme.
    * <p>
    * Note that PlayReady is unsupported by most Android devices, with the exception of Android TV
    * devices, which do provide support.
    *
    * @param playbackLooper The looper associated with the media playback thread. Should usually be
    * obtained using {@link com.google.android.exoplayer.ExoPlayer#getPlaybackLooper()}.
    * @param callback Performs key and provisioning requests.
    * @param customData Optional custom data to include in requests generated by the instance.
    * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
    * null if delivery of events is not required.
    * @param eventListener A listener of events. May be null if delivery of events is not required.
    * @throws UnsupportedDrmException If the specified DRM scheme is not supported.
    */
    public static CachedDrmSessionManager newPlayReadyInstance(Context context, Looper playbackLooper,
    MediaDrmCallback callback, String customData, Handler eventHandler,
    EventListener eventListener) throws UnsupportedDrmException {
    HashMap<String, String> optionalKeyRequestParameters;
    if (!TextUtils.isEmpty(customData)) {
    optionalKeyRequestParameters = new HashMap<>();
    optionalKeyRequestParameters.put(PLAYREADY_CUSTOM_DATA_KEY, customData);
    } else {
    optionalKeyRequestParameters = null;
    }
    return new CachedDrmSessionManager(context, PLAYREADY_UUID, playbackLooper, callback,
    optionalKeyRequestParameters, eventHandler, eventListener);
    }

    /**
    * @param uuid The UUID of the drm scheme.
    * @param playbackLooper The looper associated with the media playback thread. Should usually be
    * obtained using {@link com.google.android.exoplayer.ExoPlayer#getPlaybackLooper()}.
    * @param callback Performs key and provisioning requests.
    * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
    * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
    * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
    * null if delivery of events is not required.
    * @param eventListener A listener of events. May be null if delivery of events is not required.
    * @throws UnsupportedDrmException If the specified DRM scheme is not supported.
    */
    public CachedDrmSessionManager(Context context, UUID uuid, Looper playbackLooper, MediaDrmCallback callback,
    HashMap<String, String> optionalKeyRequestParameters, Handler eventHandler,
    EventListener eventListener) throws UnsupportedDrmException {
    this.mContext = context;
    this.uuid = uuid;
    this.callback = callback;
    this.optionalKeyRequestParameters = optionalKeyRequestParameters;
    this.eventHandler = eventHandler;
    this.eventListener = eventListener;
    try {
    mediaDrm = new MediaDrm(uuid);
    } catch (UnsupportedSchemeException e) {
    throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME, e);
    } catch (Exception e) {
    throw new UnsupportedDrmException(UnsupportedDrmException.REASON_INSTANTIATION_ERROR, e);
    }
    mediaDrm.setOnEventListener(new MediaDrmEventListener());
    mediaDrmHandler = new MediaDrmHandler(playbackLooper);
    postResponseHandler = new PostResponseHandler(playbackLooper);
    state = STATE_CLOSED;
    }

    @Override
    public final int getState() {
    return state;
    }

    @Override
    public final MediaCrypto getMediaCrypto() {
    if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
    throw new IllegalStateException();
    }
    return mediaCrypto;
    }

    @Override
    public boolean requiresSecureDecoderComponent(String mimeType) {
    if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
    throw new IllegalStateException();
    }
    return mediaCrypto.requiresSecureDecoderComponent(mimeType);
    }

    @Override
    public final Exception getError() {
    return state == STATE_ERROR ? lastException : null;
    }

    /**
    * Provides access to {@link MediaDrm#getPropertyString(String)}.
    * <p>
    * This method may be called when the manager is in any state.
    *
    * @param key The key to request.
    * @return The retrieved property.
    */
    public final String getPropertyString(String key) {
    return mediaDrm.getPropertyString(key);
    }

    /**
    * Provides access to {@link MediaDrm#setPropertyString(String, String)}.
    * <p>
    * This method may be called when the manager is in any state.
    *
    * @param key The property to write.
    * @param value The value to write.
    */
    public final void setPropertyString(String key, String value) {
    mediaDrm.setPropertyString(key, value);
    }

    /**
    * Provides access to {@link MediaDrm#getPropertyByteArray(String)}.
    * <p>
    * This method may be called when the manager is in any state.
    *
    * @param key The key to request.
    * @return The retrieved property.
    */
    public final byte[] getPropertyByteArray(String key) {
    return mediaDrm.getPropertyByteArray(key);
    }

    /**
    * Provides access to {@link MediaDrm#setPropertyByteArray(String, byte[])}.
    * <p>
    * This method may be called when the manager is in any state.
    *
    * @param key The property to write.
    * @param value The value to write.
    */
    public final void setPropertyByteArray(String key, byte[] value) {
    mediaDrm.setPropertyByteArray(key, value);
    }

    @Override
    public final void open(DrmInitData drmInitData) {
    if (++openCount != 1) {
    return;
    }
    if (postRequestHandler == null) {
    requestHandlerThread = new HandlerThread("DrmRequestHandler");
    requestHandlerThread.start();
    postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
    }
    if (schemeData == null) {
    mimeType = drmInitData.mimeType;
    schemeData = drmInitData.get(uuid);
    if (schemeData == null) {
    onError(new IllegalStateException("Media does not support uuid: " + uuid));
    return;
    }
    if (Util.SDK_INT < 21) {
    // Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
    byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeData, WIDEVINE_UUID);
    if (psshData == null) {
    // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
    } else {
    schemeData = psshData;
    }
    }
    }
    state = STATE_OPENING;
    openInternal(true);
    }

    @Override
    public final void close() {
    if (--openCount != 0) {
    return;
    }
    state = STATE_CLOSED;
    provisioningInProgress = false;
    mediaDrmHandler.removeCallbacksAndMessages(null);
    postResponseHandler.removeCallbacksAndMessages(null);
    postRequestHandler.removeCallbacksAndMessages(null);
    postRequestHandler = null;
    requestHandlerThread.quit();
    requestHandlerThread = null;
    schemeData = null;
    mediaCrypto = null;
    lastException = null;
    if (sessionId != null) {
    mediaDrm.closeSession(sessionId);
    sessionId = null;
    }
    }

    private void openInternal(boolean allowProvisioning) {


    try {
    sessionId = mediaDrm.openSession();

    mediaCrypto = new MediaCrypto(uuid, sessionId);
    state = STATE_OPENED;

    postKeyRequest();


    } catch (NotProvisionedException e) {
    if (allowProvisioning) {
    postProvisionRequest();
    } else {
    onError(e);
    }
    } catch (Exception e) {
    onError(e);
    }

    }

    private void postProvisionRequest() {
    if (provisioningInProgress) {
    return;
    }
    Log.d(TAG, "postProvisionRequest()");
    provisioningInProgress = true;
    ProvisionRequest request = mediaDrm.getProvisionRequest();
    postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget();
    }

    private void onProvisionResponse(Object response) {
    provisioningInProgress = false;
    if (state != STATE_OPENING && state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
    // This event is stale.
    return;
    }

    if (response instanceof Exception) {
    onError((Exception) response);
    return;
    }

    try {
    mediaDrm.provideProvisionResponse((byte[]) response);
    if (state == STATE_OPENING) {
    openInternal(false);
    } else {
    postKeyRequest();
    }
    } catch (DeniedByServerException e) {
    onError(e);
    }
    }

    private void postKeyRequest() {
    KeyRequest keyRequest;
    try {
    keyRequest = mediaDrm.getKeyRequest(sessionId, schemeData, mimeType,
    MediaDrm.KEY_TYPE_OFFLINE, optionalKeyRequestParameters);

    String schemeDataString = Base64.encodeToString(schemeData, Base64.DEFAULT);

    SharedPreferences settings = mContext.getSharedPreferences("PREFS_NAME", 0);
    // SharedPreferences.Editor editor = settings.edit();

    String keySetId = settings.getString(schemeDataString, "");

    if (!"".equals(keySetId)) {
    mediaDrm.restoreKeys(sessionId, Base64.decode(keySetId, Base64.DEFAULT));
    // postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
    state = STATE_OPENED_WITH_KEYS;
    }else{
    postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();

    }





    } catch (NotProvisionedException e) {
    onKeysError(e);
    }
    }

    private void onKeyResponse(Object response) {
    if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
    // This event is stale.
    return;
    }

    if (response instanceof Exception) {
    onKeysError((Exception) response);
    return;
    }

    try {
    byte[] keyResponse = mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
    String keyResponseString = Base64.encodeToString(keyResponse, Base64.DEFAULT);

    String schemeDataString = Base64.encodeToString(schemeData, Base64.DEFAULT);

    Log.d(TAG, "keySetId: " + keyResponseString);

    SharedPreferences settings = mContext.getSharedPreferences("PREFS_NAME", 0);
    SharedPreferences.Editor editor = settings.edit();
    editor.putString(schemeDataString, keyResponseString);

    editor.apply();


    state = STATE_OPENED_WITH_KEYS;
    } catch (Exception e) {
    onKeysError(e);
    }
    }

    private void onKeysError(Exception e) {
    if (e instanceof NotProvisionedException) {
    postProvisionRequest();
    } else {
    onError(e);
    }
    }

    private void onError(final Exception e) {
    lastException = e;
    if (eventHandler != null && eventListener != null) {
    eventHandler.post(new Runnable() {
    @Override
    public void run() {
    eventListener.onDrmSessionManagerError(e);
    }
    });
    }
    if (state != STATE_OPENED_WITH_KEYS) {
    state = STATE_ERROR;
    }
    }

    @SuppressLint("HandlerLeak")
    private class MediaDrmHandler extends Handler {

    public MediaDrmHandler(Looper looper) {
    super(looper);
    }

    @Override
    public void handleMessage(Message msg) {
    if (openCount == 0 || (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS)) {
    return;
    }
    switch (msg.what) {
    case MediaDrm.EVENT_KEY_REQUIRED:
    postKeyRequest();
    return;
    case MediaDrm.EVENT_KEY_EXPIRED:
    state = STATE_OPENED;
    onError(new KeysExpiredException());
    return;
    case MediaDrm.EVENT_PROVISION_REQUIRED:
    state = STATE_OPENED;
    postProvisionRequest();
    return;
    }
    }

    }

    private class MediaDrmEventListener implements OnEventListener {

    @Override
    public void onEvent(MediaDrm md, byte[] sessionId, int event, int extra, byte[] data) {
    mediaDrmHandler.sendEmptyMessage(event);
    }

    }

    @SuppressLint("HandlerLeak")
    private class PostResponseHandler extends Handler {

    public PostResponseHandler(Looper looper) {
    super(looper);
    }

    @Override
    public void handleMessage(Message msg) {
    switch (msg.what) {
    case MSG_PROVISION:
    onProvisionResponse(msg.obj);
    return;
    case MSG_KEYS:
    onKeyResponse(msg.obj);
    return;
    }
    }

    }

    @SuppressLint("HandlerLeak")
    private class PostRequestHandler extends Handler {

    public PostRequestHandler(Looper backgroundLooper) {
    super(backgroundLooper);
    }

    @Override
    public void handleMessage(Message msg) {
    Object response;


    try {
    switch (msg.what) {
    case MSG_PROVISION:
    // TODO: 26/11/2015 store and restore

    response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj);
    break;
    case MSG_KEYS:
    // TODO: 26/11/2015 store and restore

    response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj);
    Log.d(TAG, "callback.executeKeyRequest(uuid, (KeyRequest) msg.obj)");
    break;
    default:
    throw new RuntimeException();
    }
    } catch (Exception e) {
    response = e;
    }
    postResponseHandler.obtainMessage(msg.what, response).sendToTarget();
    }

    }
    }