4

I'm trying to implement offline DRM support for ExoPlayer 2 but I have some problems.

I found this conversation. There is some implementation for ExoPlayer 1.x and some steps how to work that implementation with ExoPlayer 2.x.

I have I problem with OfflineDRMSessionManager whitch implements DrmSessionManager. In that example is DrmSessionManager imported from ExoPlayer 1.x. If I import it from ExoPlayer 2 then I have a problems to compile it. I have a problem with @Override methods (open(), close(), ..) which are NOT in that new DrmSessionManager and there are some new methods: acquireSession(), ... .

Andrii Omelchenko
  • 13,183
  • 12
  • 43
  • 79
Pepa Zapletal
  • 2,879
  • 3
  • 39
  • 69

3 Answers3

5

With the latest release of ExoPlayer 2.2.0 , it provides this facility inbuilt in ExoPlayer. ExoPlayer has a helper class to download and refresh offline license keys. It should be the preferred way to do this.

OfflineLicenseHelper.java
/**
 * Helper class to download, renew and release offline licenses. It utilizes {@link
 * DefaultDrmSessionManager}.
 */
public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {

You can access the latest code from the ExoPlayer repo

I created a sample application for Offline playback of DRM content.You can access it from here

theJango
  • 1,100
  • 10
  • 22
  • 1
    Do you have a working sample? How do you set the license? – Gabriel Feb 23 '17 at 23:58
  • @Gabriel Yes I can create one for you if you still need it. – theJango Mar 08 '17 at 22:02
  • yes please. Specifically I'd like to see a working sample for license handling part. – Gabriel Mar 10 '17 at 23:48
  • @Gabriel Hey guys, I was wondering if you could share the example with me, I've been trying to make this work with no luck. Also, any of you can point me in the right direction on how to download a media file for online playback (I managed to do it for an mpd pointing to an mp4, however, when I have to download the media as dash, I have no clue) – nosmirck May 08 '17 at 17:05
  • @nosmirck Exoplayer doesn't give you the ability to download the media file (at least not for now). You should be taking care of that yourself (using download manager). Exoplayer offline license helper class only takes care of updating, renewing and downloading the license file. – Gabriel May 09 '17 at 22:33
  • i tried your sample project it is not able to play offline content – skyshine Sep 25 '17 at 06:48
  • r: internalError [10.73, loadError] com.google.android.exoplayer2.upstream.FileDataSource$FileDataSourceException: java.io.FileNotFoundException: /storage/emulated/0/tears_h264_main_480p_2000.mp4: open failed: ENOENT (No such file or directory) – skyshine Sep 25 '17 at 07:03
1

As @TheJango explained, with the latest release of ExoPlayer 2.2.0 , it provides this facility inbuilt in ExoPlayer. However, the OfflineLicenseHelper class was designed with some VOD use case in mind. Buy a movie, save the license (download method), download the movie, load the license in a DefaultDrmSessionManager and then setMode for playback.

Another use case could be that you want to make an online streaming system where different content is using the same license (e.g. Television) for quite some time (e.g. 24hours) more intelligent. So that it never downloads a license which it already has (Suppose your DRM system charges you per license request and there will be a lot of requests for the same license otherwise), the following approach can be used with ExoPlayer 2.2.0. It took me some time to get a working solution without modifying anything to the ExoPlayer source. I don't quite like the approach they've taken with the setMode() method which can only be called once. Previously DrmSessionManagers would work for multiple sessions (audio, video) and now they no longer work if licenses differ or come from different methods (DOWNLOAD, PLAYBACK, ...). Anyway, I introduced a new class CachingDefaultDrmSessionManager to replace the DefaultDrmSessionManager you are probably using. Internally it delegates to a DefaultDrmSessionManager.

package com.google.android.exoplayer2.drm;

import android.content.Context;
import android.content.SharedPreferences;
import java.util.concurrent.atomic.AtomicBoolean;
import android.os.Handler;
import android.os.Looper;
import android.util.Base64;
import android.util.Log;

import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Util;

import java.util.Arrays;
import java.util.HashMap;
import java.util.UUID;

import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_DOWNLOAD;
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_QUERY;

public class CachingDefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T> {

    private final SharedPreferences drmkeys;
    public static final String TAG="CachingDRM";
    private final DefaultDrmSessionManager<T> delegateDefaultDrmSessionManager;
    private final UUID uuid;
    private final AtomicBoolean pending = new AtomicBoolean(false);
    private byte[] schemeInitD;

    public interface EventListener {
        void onDrmKeysLoaded();
        void onDrmSessionManagerError(Exception e);
        void onDrmKeysRestored();
        void onDrmKeysRemoved();
    }

    public CachingDefaultDrmSessionManager(Context context, UUID uuid, ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters, final Handler eventHandler, final EventListener eventListener) {
        this.uuid = uuid;
        DefaultDrmSessionManager.EventListener eventListenerInternal = new DefaultDrmSessionManager.EventListener() {

            @Override
            public void onDrmKeysLoaded() {
                saveDrmKeys();
                pending.set(false);
                if (eventListener!=null) eventListener.onDrmKeysLoaded();
            }

            @Override
            public void onDrmSessionManagerError(Exception e) {
                pending.set(false);
                if (eventListener!=null) eventListener.onDrmSessionManagerError(e);
            }

            @Override
            public void onDrmKeysRestored() {
                saveDrmKeys();
                pending.set(false);
                if (eventListener!=null) eventListener.onDrmKeysRestored();
            }

            @Override
            public void onDrmKeysRemoved() {
                pending.set(false);
                if (eventListener!=null) eventListener.onDrmKeysRemoved();
            }
        };
        delegateDefaultDrmSessionManager = new DefaultDrmSessionManager<T>(uuid, mediaDrm, callback, optionalKeyRequestParameters, eventHandler, eventListenerInternal);
        drmkeys = context.getSharedPreferences("drmkeys", Context.MODE_PRIVATE);
    }

    final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
    public static String bytesToHex(byte[] bytes) {
        char[] hexChars = new char[bytes.length * 2];
        for ( int j = 0; j < bytes.length; j++ ) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = hexArray[v >>> 4];
            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        return new String(hexChars);
    }

    public void saveDrmKeys() {
        byte[] offlineLicenseKeySetId = delegateDefaultDrmSessionManager.getOfflineLicenseKeySetId();
        if (offlineLicenseKeySetId==null) {
            Log.i(TAG,"Failed to download offline license key");
        } else {
            Log.i(TAG,"Storing downloaded offline license key for "+bytesToHex(schemeInitD)+": "+bytesToHex(offlineLicenseKeySetId));
            storeKeySetId(schemeInitD, offlineLicenseKeySetId);
        }
    }

    @Override
    public DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData) {
        if (pending.getAndSet(true)) {
             return delegateDefaultDrmSessionManager.acquireSession(playbackLooper, drmInitData);
        }
        // First check if we already have this license in local storage and if it's still valid.
        DrmInitData.SchemeData schemeData = drmInitData.get(uuid);
        schemeInitD = schemeData.data;
        Log.i(TAG,"Request for key for init data "+bytesToHex(schemeInitD));
        if (Util.SDK_INT < 21) {
            // Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
            byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitD, C.WIDEVINE_UUID);
            if (psshData == null) {
                // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
            } else {
                schemeInitD = psshData;
            }
        }
        byte[] cachedKeySetId=loadKeySetId(schemeInitD);
        if (cachedKeySetId!=null) {
            //Load successful.
            Log.i(TAG,"Cached key set found "+bytesToHex(cachedKeySetId));
            if (!Arrays.equals(delegateDefaultDrmSessionManager.getOfflineLicenseKeySetId(), cachedKeySetId))
            {
                delegateDefaultDrmSessionManager.setMode(MODE_QUERY, cachedKeySetId);
            }
        } else {
            Log.i(TAG,"No cached key set found ");
            delegateDefaultDrmSessionManager.setMode(MODE_DOWNLOAD,null);
        }
        DrmSession<T> tDrmSession = delegateDefaultDrmSessionManager.acquireSession(playbackLooper, drmInitData);
        return tDrmSession;
    }

    @Override
    public void releaseSession(DrmSession<T> drmSession) {
        pending.set(false);
        delegateDefaultDrmSessionManager.releaseSession(drmSession);
    }

    public void storeKeySetId(byte[] initData, byte[] keySetId) {
        String encodedInitData = Base64.encodeToString(initData, Base64.NO_WRAP);
        String encodedKeySetId = Base64.encodeToString(keySetId, Base64.NO_WRAP);
        drmkeys.edit()
                .putString(encodedInitData, encodedKeySetId)
                .apply();
    }

    public byte[] loadKeySetId(byte[] initData) {
        String encodedInitData = Base64.encodeToString(initData, Base64.NO_WRAP);
        String encodedKeySetId = drmkeys.getString(encodedInitData, null);
        if (encodedKeySetId == null) return null;
        return Base64.decode(encodedKeySetId, 0);
    }

}

Here keys are persisted as Base64 encoded strings in local storage. Because for a typical DASH stream both audio and video renderers will request a license from the DrmSessionManager, possibly at the same time, the AtomicBoolean is used. If audio and or video would use different keys, I think this approach would fail. Also I am not yet checking for expired keys here. Have a look at OfflineLicenseHelper to see how to deal with those.

Jeroen Ost
  • 351
  • 2
  • 7
0

@Pepa Zapletal, proceed with below changes to play in offline.

You can also see the updated answer here.

Changes are as follows :

  1. Changed signature of the method private void onKeyResponse(Object response) to private void onKeyResponse(Object response, boolean offline)

  2. Rather than sending the file manifest URI send stored file path to PlayerActivity.java.

  3. Change MediaDrm.KEY_TYPE_STREAMING to MediaDrm.KEY_TYPE_OFFLINE in getKeyRequest().

  4. In postKeyRequest() first check whether the key is stored or not, if key found then directly call onKeyResponse(key, true).
  5. In onKeyResponse(), call restoreKeys() rather than calling provideKeyResponse().
  6. The rest everything is same, now your file will be playing.

Major role : Here provideKeyResponse() and restoreKeys() are native methods which acts major role in getting the key and restoring the key.

provideKeyResponse() method which will return us the main License key in byte array if and only if the keyType is MediaDrm.KEY_TYPE_OFFLINE else this method will return us the empty byte array with which we can do nothing with that array.

restoreKeys() method will expect the key which is to be restored for the current session, so feed the key which we have already stored in local to this method and it will take care of it.

Note : First you have to somehow download the license key and store it somewhere in local device securely.

In my case first im playing the file online, so exoplayer will fetch the key that key i have stored in local. From second time onwards first it will check whether the key is stored or not, if key found it will skip the License key request and will the play the file.

Replace the methods and inner classes of StreamingDrmSessionManager.java with these things.

private void postKeyRequest() {
    KeyRequest keyRequest;
    try {
        // check is key exist in local or not, if exist no need to
        // make a request License server for the key.
      byte[] keyFromLocal = Util.getKeyFromLocal();
      if(keyFromLocal != null) {
          onKeyResponse(keyFromLocal, true);
          return;
      }

      keyRequest = mediaDrm.getKeyRequest(sessionId, schemeData.data, schemeData.mimeType, MediaDrm.KEY_TYPE_OFFLINE, optionalKeyRequestParameters);
      postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
    } catch (NotProvisionedException e) {
      onKeysError(e);
    }
  }


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

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

    try {
        // if we have a key and we want to play offline then call 
        // 'restoreKeys()' with the key which we have already stored.
        // Here 'response' is the stored key. 
        if(offline) {
            mediaDrm.restoreKeys(sessionId, (byte[]) response);
        } else {
            // Don't have any key in local, so calling 'provideKeyResponse()' to
            // get the main License key and store the returned key in local.
            byte[] bytes = mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
            Util.storeKeyInLocal(bytes);
        }
      state = STATE_OPENED_WITH_KEYS;
      if (eventHandler != null && eventListener != null) {
        eventHandler.post(new Runnable() {
          @Override
          public void run() {
            eventListener.onDrmKeysLoaded();
          }
        });
      }
    } catch (Exception e) {
      onKeysError(e);
    }
  }


@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);
          break;
        case MSG_KEYS:
          // We don't have key in local so calling 'onKeyResponse()' with offline to 'false'.
          onKeyResponse(msg.obj, false);
          break;
      }
    }

  }
Community
  • 1
  • 1
Abilash
  • 218
  • 3
  • 10