2016-12-02 209 views
3

我試圖爲ExoPlayer 2實現離線DRM支持,但我遇到了一些問題。Android - ExoPlayer 2離線播放DRM(widevine)

我發現這個conversation。 ExoPlayer 1.x有一些實現以及如何使用ExoPlayer 2.x來實現該實現的一些步驟。

我有我OfflineDRMSessionManager whitch執行DrmSessionManager的問題。在這個例子中,DrmSessionManager從ExoPlayer 1.x中導入。如果我從ExoPlayer 2導入它,那麼我有一個編譯它的問題。我在@Override方法(open(),close(),..)中遇到問題,它們不在新的DrmSessionManager中,並且有一些新方法:acquireSession(),...。

回答

-1

@Pepa Zapletal,繼續進行以下更改以在離線狀態下播放。

您還可以看到更新的答案here

變化如下

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

  2. 而不是清單URI發送保存的文件路徑的文件發送到PlayerActivity.java.

  3. 變化MediaDrm.KEY_TYPE_STREAMINGMediaDrm.KEY_TYPE_OFFLINE於​​。

  4. postKeyRequest()首先檢查關鍵字是否儲存或沒有,如果鑰匙上發現然後直接調用onKeyResponse(key, true)
  5. onKeyResponse(),叫restoreKeys()而不是調用provideKeyResponse()
  6. 其餘的一切都是一樣的,現在你的文件將被播放。

主要作用:這裏provideKeyResponse()restoreKeys()是作用在拿到鑰匙和恢復關鍵的重要作用本地方法。

provideKeyResponse()方法將返回我們的主要許可證的字節數組鍵當且僅當keytype是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; 
     } 
    } 

    } 
3

使用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 repo

我創建了DRM content.You的離線播放可以從here

+1

你有工作樣本嗎?你如何設置許可證? – Gabriel

+0

@Gabriel是的,如果你還需要它,我可以爲你創建一個。 – theJango

+0

是的。具體而言,我希望看到許可證處理部分的工作示例。 – Gabriel

0

訪問它@TheJango解釋的一個示例應用程序訪問最新的代碼,用最新ExoPlayer 2.2.0的發佈,它提供了這個內置於ExoPlayer中的設施。 但是,OfflineLicenseHelper課程設計時考慮了一些VOD用例。購買電影,保存許可證(下載方法),下載電影,將許可證載入DefaultDrmSessionManager,然後將其設置爲播放模式。

另一個用例可能是您想要製作一個在線流式傳輸系統,其中不同內容在相當長時間(例如24小時)內使用相同許可證(例如電視)更加智能。因此,它永遠不會下載它已經擁有的許可證(假設您的DRM系統根據許可證請求收取費用,否則會有相同許可證的許多請求),以下方法可用於ExoPlayer 2.2.0。我花了一些時間纔得到一個工作解決方案,而無需對ExoPlayer源進行任何修改。我不太喜歡他們採用setMode()方法的方法,該方法只能調用一次。以前的DrmSessionManager會適用於多個會話(音頻,視頻),現在如果許可證不同或來自不同的方法(下載,播放,...),它們現在不再工作。無論如何,我介紹了一個新的類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 = "ABCDEF".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看看如何處理這些。