3

我正在尝试为 ExoPlayer 2 实现离线 DRM 支持,但我遇到了一些问题。

我找到了这个对话。ExoPlayer 1.x 有一些实现,以及如何与 ExoPlayer 2.x 一起使用该实现的一些步骤。

我对OfflineDRMSessionManagerwhitch 工具有疑问DrmSessionManager。在该示例中是从 ExoPlayer 1.x 导入的 DrmSessionManager。如果我从 ExoPlayer 2 导入它,那么我在编译它时会遇到问题。我对@Override不在那个新 DrmSessionManager 中的方法(open()、close()、..)有疑问,并且有一些新方法:acquireSession(), ... 。

4

3 回答 3

4

随着 ExoPlayer 2.2.0 的最新版本,它提供了内置在 ExoPlayer 中的此功能。ExoPlayer 有一个帮助类来下载和刷新离线许可证密钥。这应该是执行此操作的首选方式。

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

您可以从ExoPlayer 存储库访问最新代码

我为 DRM 内容的离线播放创建了一个示例应用程序。您可以从这里访问它

于 2017-02-21T04:56:56.173 回答
0

正如@TheJango 解释的那样,在最新版本的 ExoPlayer 2.2.0 中,它提供了内置在 ExoPlayer 中的此功能。但是,该OfflineLicenseHelper课程的设计考虑了一些 VOD 用例。购买电影,保存license(下载方式),下载电影,在a中加载license,DefaultDrmSessionManager然后setMode进行播放。

另一个用例可能是您想要制作一个在线流媒体系统,其中不同的内容使用相同的许可证(例如电视)相当长一段时间(例如 24 小时)更加智能。因此它永远不会下载它已经拥有的许可证(假设您的 DRM 系统向您收取每个许可证请求的费用,否则会有很多相同许可证的请求),以下方法可以与 ExoPlayer 2.2.0 一起使用。我花了一些时间才得到一个可行的解决方案,而无需对 ExoPlayer 源进行任何修改。我不太喜欢他们使用setMode()只能调用一次的方法所采用的方法。之前DrmSessionManagers 适用于多个会话(音频、视频),现在如果许可证不同或来自不同方法(下载、播放、...),它们将不再工作。无论如何,我介绍了一个新类CachingDefaultDrmSessionManager来替换DefaultDrmSessionManager您可能正在使用的类。在内部它委托给一个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);
    }

}

这里的键作为 Base64 编码的字符串保存在本地存储中。因为对于典型的 DASH 流,音频和视频渲染器都会从 请求许可证DrmSessionManager,可能同时AtomicBoolean使用。如果音频和/或视频使用不同的键,我认为这种方法会失败。此外,我还没有在这里检查过期的密钥。看看OfflineLicenseHelper如何处理这些。

于 2017-03-08T10:48:53.133 回答
-1

@Pepa Zapletal,继续进行以下更改以离线播放。

您还可以在此处查看更新的答案。

变化如下

  1. 将方法的签名更改private void onKeyResponse(Object response)private void onKeyResponse(Object response, boolean offline)

  2. 而不是发送文件清单 URI,而是将存储的文件路径发送到PlayerActivity.java.

  3. 更改MediaDrm.KEY_TYPE_STREAMING为. MediaDrm.KEY_TYPE_OFFLINE_getKeyRequest()

  4. postKeyRequest()首先检查是否存储了密钥,如果找到密钥则直接调用onKeyResponse(key, true)
  5. onKeyResponse(),调用restoreKeys()而不是调用provideKeyResponse()
  6. 其余的一切都是一样的,现在你的文件将被播放。

主要作用:这里provideKeyResponse()restoreKeys()是在获取密钥和恢复密钥方面起主要作用的本地方法。

provideKeyResponse()当且仅当 keyType 为 else 时,该方法将向我们返回字节数组中的主许可证密钥MediaDrm.KEY_TYPE_OFFLINE,该方法将返回空字节数组,我们无法对该数组执行任何操作。

restoreKeys()方法将期望为当前会话恢复的密钥,因此将我们已经存储在本地的密钥提供给该方法,它会处理它。

注意: 首先,您必须以某种方式下载许可证密钥并将其安全地存储在本地设备中的某个位置。

在我的情况下,首先我在线播放文件,所以 exoplayer 将获取我存储在本地的密钥。从第二次开始,它首先会检查是否存储了密钥,如果找到密钥,它将跳过许可证密钥请求并播放文件。

StreamingDrmSessionManager.java用这些东西替换方法和内部类。

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;
      }
    }

  }
于 2017-01-13T20:22:12.147 回答