Android — ExoPlayer 2 воспроизводит DRM (widevine) в автономном режиме

Я пытаюсь реализовать автономную поддержку DRM для ExoPlayer 2, но у меня есть некоторые проблемы.

Я нашел этот диалог. Существует некоторая реализация для ExoPlayer 1.x и несколько шагов, как работать с этой реализацией с ExoPlayer 2.x.

У меня проблема с OfflineDRMSessionManager, который реализует DrmSessionManager. В этом примере DrmSessionManager импортируется из ExoPlayer 1.x. Если я импортирую его из ExoPlayer 2, у меня возникнут проблемы с его компиляцией. У меня проблема с @Override методами (open(), close(), ..), которых НЕТ в этом новом DrmSessionManager, и есть несколько новых методов: acquireSession(), ... .


person Pepa Zapletal    schedule 02.12.2016    source источник


Ответы (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.

Я создал пример приложения для автономного воспроизведения содержимого DRM. Вы можете получить к нему доступ здесь

person theJango    schedule 21.02.2017
comment
У вас есть рабочий образец? Как вы устанавливаете лицензию? - person Gabriel; 24.02.2017
comment
@Gabriel Да, я могу создать его для вас, если он вам все еще нужен. - person theJango; 09.03.2017
comment
Да, пожалуйста. В частности, я хотел бы увидеть рабочий образец для части обработки лицензий. - person Gabriel; 11.03.2017
comment
@Gabriel Привет, ребята, мне было интересно, не могли бы вы поделиться со мной примером, я безуспешно пытался заставить это работать. Кроме того, любой из вас может указать мне правильное направление о том, как загрузить медиафайл для онлайн-воспроизведения (мне удалось сделать это для mpd, указывающего на mp4, однако, когда мне нужно загрузить медиа как тире, у меня есть без понятия) - person nosmirck; 08.05.2017
comment
@nosmirck Exoplayer не дает вам возможности загрузить медиафайл (по крайней мере, пока). Вы должны позаботиться об этом сами (используя менеджер загрузок). Вспомогательный класс автономной лицензии Exoplayer занимается только обновлением, обновлением и загрузкой файла лицензии. - person Gabriel; 10.05.2017
comment
я попробовал ваш пример проекта, он не может воспроизводить автономный контент - person skyshine; 25.09.2017
comment
r: internalError [10.73, loadError] com.google.android.exoplayer2.upstream.FileDataSource$FileDataSourceException: java.io.FileNotFoundException: /storage/emulated/0/tears_h264_main_480p_2000.mp4: ошибка открытия: ENOENT (Нет такого файла или каталога) - person skyshine; 25.09.2017

Как объяснил @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 = "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, чтобы узнать, как с ними справиться.

person Jeroen Ost    schedule 08.03.2017

@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(), который вернет нам основной Лицензионный ключ в массиве байтов тогда и только тогда, когда тип ключа равен 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;
      }
    }

  }
person Abilash    schedule 13.01.2017