当前位置: 首页 > 知识库问答 >
问题:

Android-ExoPlayer 2离线播放DRM(widevine)

有宏峻
2023-03-14

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

我找到了这段对话。ExoPlayer 1有一些实现。以及如何使用ExoPlayer 2实现的一些步骤。十、

我对离线DrmSessionManager有问题,which实现了DrmSessionManager。在该示例中,是从ExoPlayer 1导入的DrmSessionManager。x、 如果我从ExoPlayer 2导入它,那么我在编译它时会遇到问题。我对重写方法(open()、close()、…)有问题这不在新的DrmSessionManager中,还有一些新方法:acquireSession()

共有3个答案

熊朝
2023-03-14

@佩帕·扎普勒(Pepa Zapletal),继续进行以下更改以在脱机状态下玩。

你也可以在这里看到更新的答案。

变更如下:

>

  • 将方法的签名更改为私有void onKeyResponse(Object response)为私有void onKeyResponse(Object response,boolean offline)

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

    更改MediaDrm。将\u TYPE\u STREAMING键转到MediaDrm。在getKeyRequest()中键入脱机键。

    主要作用:这里的provideKeyResponse()和restoreKeys()是本机方法,在获取密钥和还原密钥方面起主要作用。

    provideKeyResponse()方法,当且仅当密钥类型为MediaDrm时,该方法将返回字节数组中的主许可证密钥。KEY\u TYPE\u 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;
          }
        }
    
      }
    

  • 巢安澜
    2023-03-14

    正如@TheJango所解释的,在最新版本的ExoPlayer 2.2.0中,它提供了内置在ExoPlayer中的这个功能。然而,OfflineLicenseHelper类的设计考虑了一些VOD用例。购买电影,保存许可证(下载方法),下载电影,在DefaultDrmSessionManager中加载许可证,然后设置播放模式。

    另一个用例可能是您希望使不同内容在相当长一段时间(例如24小时)内使用相同许可证(例如电视)的在线流媒体系统更加智能。因此它永远不会下载它已经拥有的许可证(假设您的DRM系统根据许可证请求向您收费,否则会有很多对相同许可证的请求),以下方法可用于ExoPlayer 2.2.0。我花了一些时间才获得一个工作解决方案,而无需修改ExoPlayer源代码。我不太喜欢他们对setMode()方法采取的方法,该方法只能调用一次。以前DrmSessionManagers适用于多个会话(音频、视频),现在如果许可证不同或来自不同的方法(DOWNLOAD、PLAYBACK、...),它们将不再工作。无论如何,我引入了一个新的类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编码字符串保存在本地存储中。因为对于典型的仪表盘数据流,音频和视频渲染器都将从DrmSessionManager请求许可证,可能同时使用原子布尔值。如果音频和/或视频使用不同的键,我认为这种方法将失败。此外,我还没有在这里检查过期的密钥。请查看OfflineLicenseHelper,了解如何处理这些问题。

    严扬
    2023-03-14

    在最新版本的ExoPlayer 2.2.0中,它提供了内置在ExoPlayer中的此功能。ExoPlayer有一个helper类来下载和刷新脱机许可证密钥。这应该是最好的方法。

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

    您可以从ExoPlayer repo访问最新代码

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

     类似资料:
    • 我一直在阅读有关在chromecast上连接设备后让widevine播放的内容。我已经让widevine在android和ios上为电影标题工作,我不知道从哪里开始,因为我应该将我的自定义数据传递给播放器。我在 https://github.com/googlecast/CastMediaPlayerStreamingDRM 但我还没有尝试实现它,因为我不想旋转轮子,试图将随机键和东西传递给接收器

    • 我有一个要求,我需要使用Android media player播放widevine DRM内容。我还听说过一种叫做ExoPlayer的东西,它内置了对widevine DRM内容的支持。 首先,我想知道使用上述任一媒体播放器开始播放此类内容所需的先决条件是什么。

    • 我无法使用exoplayer2.2离线播放歌曲。 这是我的密码。 我得到以下错误。 谁能帮忙吗。

    • 我无法通过ExoPlayer框架运行DRM PlayReady内容。我想这与我的drmSessionManager有关 清单Url和LicenseUrl都已经过测试,但很难实现我的更改,因为没有真正的具体示例实现任何DRM内容(WideVine或PlayReady) 正常的Dash内容与ExoPlayer配合使用,但当我尝试播放任何受DRM保护的内容时,它失败了。 构建实际drmSessionMa

    • 如何在ExoPlayer2上设置字幕?我试过这个媒体来源: 但我犯了一个错误:

    • 我已使用Azure媒体服务存储加密视频 有人能帮我解决以下问题吗: 我从哪里获得Widevine许可证URL,以便在Exo Player中播放视频 提前感谢。