Exoplayer: Offline DRM protection

Created on 11 Nov 2015  Â·  58Comments  Â·  Source: google/ExoPlayer

Hi,
Any news on offline DRM protection? Is it possible to decrypt and play a widevine protected movie that is stored on the phone with no internet access?

question

Most helpful comment

Does anyone interested in the same implementation for the ExoPlayer v2 ?

All 58 comments

Several days after I will do some work about DRM.

Offline DRM Protection is Possible via Android DRM SDK, You just have to open the DRM Session on your own.

Extend your class from AbstractDrmSessionManager.

The Keypoint to guide you is in your MediaDrm.KeyRequest the keyType must be of type MediaDrm.KEY_TYPE_OFFLINE

You can find more informations about this here:
http://developer.android.com/reference/android/media/MediaDrm.html#KEY_TYPE_OFFLINE

Thank you. However, is it possible to (using exoplayer), bypass the whole server license/key acquisition step and assume licenses are stored locally? Licenses can then be retrieved offline to acquire the key and decrypt and play offline movies?

On a different note, I am testing your samples and when I select sample 4 in WIDEVINE GTS DASH (WV: secure video path required ) I get playerFailed followed by android.media.MediaCodec$CryptoException: Error decrypting data. I changed nothing in the code after downloading the zip file from Github.

Please note that I only get the error on a Samsung 4 while it works on a Samsung 6.

Hi,
I am trying to extends the player functionality for Offline Videos.The Query is should i have to write a separate ExtractorRendererBuilder providing the OfflineDRMSessionManager object or i have to use DashRendererbuilder.

Since we are using ExtractorRendererBuilder for playing offline videos, I guess i have to rewrite the same class.

Also do i need to download .mpd file along with .mp4 or only .mp4 will work..??

@Amritpal33 You don't need a new ExtractorRendererBuilder, you only need to create a new DRMSessionManager.

Yes, you should. You will provide the player the mpd file not the mp4, and the mpd should point to the mp4 file.

@sarahachem Regarding your question yes it is possible. But as stated above you will need to create your own DRMSessionManager.

@Amritpal33 - were you able to make it work?
@hakemcpu - just in case do you plan to provide any working sample for off-line playback for Widevine modular/DASH?
One day it would be great to see Samples.java is extended with off-line playback code.

Are there any plan to provide off-line sample?

I was able to make it work by writing my own DrmSessionManager that store
keys in media drm and later restore it.

Also you can use Extractor renderer builder with this SessionManager to
play your video/audio.

On Jul 20, 2016 3:25 PM, "Shintaro Katafuchi" [email protected]
wrote:

Are there any plan to provide off-line sample?

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/google/ExoPlayer/issues/949#issuecomment-233905132,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ADjdjsXC5MCennn79ZEOb9T0kb9zsIkEks5qXfCOgaJpZM4GgSz7
.

+1 for an offline DRM sample.

@Amritpal33 Thx for your info! Can you give us a sample code? And one more question, what vide format do you use?

This file will help you to fetch keys and store it in mediaDrm which in turn returns keySetId.

###
import android.annotation.SuppressLint;
import android.content.Context;
import android.media.MediaDrm;
import android.media.NotProvisionedException;
import android.media.UnsupportedSchemeException;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;

import com.google.android.exoplayer.C;
import com.google.android.exoplayer.demo.WidevineTestMediaDrmCallback;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.extractor.DefaultExtractorInput;
import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorOutput;
import com.google.android.exoplayer.extractor.ExtractorSampleSource;
import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.extractor.SeekMap;
import com.google.android.exoplayer.extractor.TrackOutput;
import com.google.android.exoplayer.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
import com.google.android.exoplayer.util.Util;
import com.jio.media.widevinesample.MainActivity;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * Created by amritpalsingh on 16/12/15.
 */
public class WidevineKeysFetcher implements ExtractorOutput
{

    PositionHolder _positionHolder;
    PostResponseHandler _postResponseHandler;

    private HandlerThread _requestHandlerThread;
    private Handler _postRequestHandler;
    private Extractor _extractor;
    private String _mimeType;
    private byte[] _schemeData;
    private byte[] _sessionId;
    private static final int MSG_KEYS = 1;
    private MediaDrm _mediaDrm;
    private static final String TAG = WidevineKeysFetcher.class.getSimpleName();
    private String _assetID;
    private Context _context;
    private static WidevineKeysFetcher _instance;
    public static final String OUTPUT_FILE_PATH="/mnt/sdcard/wv/lic.text";


    private WidevineKeysFetcher()
    {
        if (_instance != null)
        {
            throw new IllegalStateException("No two instances of this class can co-exists.");
        }
        _positionHolder = new PositionHolder();
    }

    public static WidevineKeysFetcher getInstance()
    {
        if (_instance == null)
        {
            synchronized (WidevineKeysFetcher.class)
            {
                _instance = new WidevineKeysFetcher();
            }
        }
        return _instance;
    }

    public void fetchKeys(Context context, String assetID, String userAgent, String filePath, Extractor... extractors)
    {

        _assetID = assetID;
        _context = context;

        int result = Extractor.RESULT_CONTINUE;
        DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(null, null);
        DataSource dataSource = new DefaultUriDataSource(_context, bandwidthMeter, userAgent);

        ExtractorInput input = null;
        try
        {
            long position = _positionHolder.position;
            long length = 0;

            length = dataSource.open(new DataSpec(Uri.parse(filePath), position, C.LENGTH_UNBOUNDED, null));

            if (length != C.LENGTH_UNBOUNDED)
            {
                length += position;
            }
            input = new DefaultExtractorInput(dataSource, position, length);
            if (extractors == null || extractors.length == 0)
            {
                extractors = new Extractor[ExtractorSampleSource.DEFAULT_EXTRACTOR_CLASSES.size()];
                for (int i = 0; i < extractors.length; i++)
                {
                    try
                    {
                        extractors[i] = ExtractorSampleSource.DEFAULT_EXTRACTOR_CLASSES.get(i).newInstance();
                    }
                    catch (InstantiationException e)
                    {
                        throw new IllegalStateException("Unexpected error creating default extractor", e);
                    }
                    catch (IllegalAccessException e)
                    {
                        throw new IllegalStateException("Unexpected error creating default extractor", e);
                    }
                }
            }

            ExtractorSampleSource.ExtractorHolder extractorHolder = new ExtractorSampleSource.ExtractorHolder(extractors, this);
            _extractor = extractorHolder.selectExtractor(input);

            while (result == Extractor.RESULT_CONTINUE)
            {
                result = _extractor.read(input, _positionHolder);
            }
        }

        catch (IOException e)
        {
            e.printStackTrace();
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (result == Extractor.RESULT_SEEK)
            {
                result = Extractor.RESULT_CONTINUE;
            }
            else if (input != null)
            {
                _positionHolder.position = input.getPosition();
            }

            try
            {
                dataSource.close();
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
        }
    }

    /**
     * Invoked when the {@link Extractor} identifies the existence of a track in the stream.
     * <p/>
     * Returns a {@link TrackOutput} that will receive track level data belonging to the track.
     *
     * @param trackId A track identifier.
     * @return The {@link TrackOutput} that should receive track level data belonging to the track.
     */
    @Override
    public TrackOutput track(int trackId)
    {
        return null;
    }

    /**
     * Invoked when all tracks have been identified, meaning that {@link #track(int)} will not be
     * invoked again.
     */
    @Override
    public void endTracks()
    {

    }

    /**
     * Invoked when a {@link SeekMap} has been extracted from the stream.
     *
     * @param seekMap The extracted {@link SeekMap}.
     */
    @Override
    public void seekMap(SeekMap seekMap)
    {

    }

    /**
     * Invoked when {@link DrmInitData} has been extracted from the stream.
     *
     * @param drmInitData The extracted {@link DrmInitData}.
     */
    @Override
    public void drmInitData(DrmInitData drmInitData)
    {
        createFetchKeyRequest(drmInitData);
    }

    private void initDRM()
    {
        try
        {
            _mediaDrm = new MediaDrm(OfflineDRMSessionManager.WIDEVINE_UUID);
            _sessionId = _mediaDrm.openSession();
            postKeyRequest();
        }
        catch (UnsupportedSchemeException e)
        {
            e.printStackTrace();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }


    private void postKeyRequest()
    {
        MediaDrm.KeyRequest keyRequest;
        try
        {
            keyRequest = _mediaDrm.getKeyRequest(_sessionId, _schemeData, _mimeType,
                    MediaDrm.KEY_TYPE_OFFLINE, null);
            _postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
        }
        catch (NotProvisionedException e)
        {
            // onKeysError(e);
        }
    }


    private void createFetchKeyRequest(DrmInitData drmInitData)
    {
        if (_postRequestHandler == null)
        {
            _postResponseHandler = new PostResponseHandler(_context.getMainLooper());
        }
        if (_postRequestHandler == null)
        {
            _requestHandlerThread = new HandlerThread("DrmRequestHandler");
            _requestHandlerThread.start();
            _postRequestHandler = new PostRequestHandler(_requestHandlerThread.getLooper());
        }
        if (_schemeData == null)
        {
            _mimeType = drmInitData.mimeType;
            _schemeData = drmInitData.get(OfflineDRMSessionManager.WIDEVINE_UUID);
            if (_schemeData == null)
            {
                return;
            }
            if (Util.SDK_INT < 21)
            {
                Log.i(TAG, "+++Android version<21, Reading pssh header");
                // Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
                byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(_schemeData, OfflineDRMSessionManager.WIDEVINE_UUID);
                if (psshData == null)
                {
                    // Extraction failed. _schemeData isn't a Widevine PSSH atom, so leave it unchanged.
                }
                else
                {
                    _schemeData = psshData;
                }
            }
        }
        initDRM();
    }

    @SuppressLint("HandlerLeak")
    private class PostRequestHandler extends Handler
    {

        public PostRequestHandler(Looper backgroundLooper)
        {
            super(backgroundLooper);
        }

        @Override
        public void handleMessage(Message msg)
        {
            Object response;
            try
            {
                switch (msg.what)
                {
                    case MSG_KEYS:
                        response = new WidevineTestMediaDrmCallback(_assetID).executeKeyRequest(OfflineDRMSessionManager.WIDEVINE_UUID, (MediaDrm.KeyRequest) msg.obj);
                        break;
                    default:
                        throw new RuntimeException();
                }
            }
            catch (Exception e)
            {
                response = e;
            }
            _postResponseHandler.obtainMessage(msg.what, response).sendToTarget();
        }

    }

    @SuppressLint("HandlerLeak")
    private class PostResponseHandler extends Handler
    {

        public PostResponseHandler(Looper looper)
        {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg)
        {
            switch (msg.what)
            {
                case MSG_KEYS:
                    onKeyResponse(msg.obj);
                    return;
            }
        }

    }

    private void onKeyResponse(Object response)
    {
        if (response instanceof Exception)
        {
            return;
        }

        try
        {
            byte[] keySetID = _mediaDrm.provideKeyResponse(_sessionId, (byte[]) response);
            File outputFile=new File(OUTPUT_FILE_PATH);
            if(!outputFile.exists())
            {
                outputFile.createNewFile();
            }

            FileOutputStream outputStream=new FileOutputStream(outputFile);
            outputStream.write(keySetID,0,keySetID.length);
            outputStream.flush();
            outputStream.close();

            //MainActivity.TEMP_KEYSET_ID = keySetID;
            Log.d(TAG, "++++++key set id after response" + keySetID);
        }

        catch (Exception e)
        {
            e.printStackTrace();
        }
    }

}

Here is the same Offline DRMSessionManager implementation that restore keys using keySetId which you have store somewhere in your db.

Once you get the keys

package com.google.android.exoplayer.demo.offline;

#

import android.annotation.SuppressLint;
import android.media.MediaCrypto;
import android.media.MediaDrm;
import android.media.NotProvisionedException;
import android.media.UnsupportedSchemeException;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;

import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.drm.DrmSessionManager;
import com.google.android.exoplayer.drm.KeysExpiredException;
import com.google.android.exoplayer.drm.MediaDrmCallback;
import com.google.android.exoplayer.drm.UnsupportedDrmException;

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

/**
 * Created by amritpalsingh on 16/12/15.
 */
public class OfflineDRMSessionManager implements DrmSessionManager
{

    /**
     * UUID for the Widevine DRM scheme.
     */
    public static final UUID WIDEVINE_UUID = UUID.fromString("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed");

    private final Handler eventHandler;
    private final EventListener eventListener;
    private final MediaDrm mediaDrm;
    private final HashMap<String, String> optionalKeyRequestParameters;

    /* package */ final MediaDrmHandler mediaDrmHandler;
    /* package */ final MediaDrmCallback callback;
    /* package */ final UUID uuid;

    private int openCount;
    private int state;
    private MediaCrypto mediaCrypto;
    private Exception lastException;
    private byte[] sessionId;
    private byte[] keySetID;
    private static final String TAG = OfflineDRMSessionManager.class.getSimpleName();


    /**
     * Interface definition for a callback to be notified of {@link OfflineDRMSessionManager}
     * events.
     */
    public interface EventListener
    {
        /**
         * Invoked when a drm error occurs.
         *
         * @param e The corresponding exception.
         */
        void onDrmSessionManagerError(Exception e);

    }

    /**
     * Instantiates a new instance using the Widevine scheme.
     *
     * @param playbackLooper               The looper associated with the media playback thread. Should usually be
     *                                     obtained using {@link com.google.android.exoplayer.ExoPlayer#getPlaybackLooper()}.
     * @param callback                     Performs key and provisioning requests.
     * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
     *                                     to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
     * @param eventHandler                 A handler to use when delivering events to {@code eventListener}. May be
     *                                     null if delivery of events is not required.
     * @param eventListener                A listener of events. May be null if delivery of events is not required.
     * @throws UnsupportedDrmException If the specified DRM scheme is not supported.
     */
    public static OfflineDRMSessionManager newWidevineInstance(Looper playbackLooper,
                                                               MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters,
                                                               Handler eventHandler, EventListener eventListener,byte[] keySetId) throws UnsupportedDrmException
    {
        return new OfflineDRMSessionManager(WIDEVINE_UUID, playbackLooper, callback,
                optionalKeyRequestParameters, eventHandler, eventListener,keySetId);
    }

    /**
     * @param uuid                         The UUID of the drm scheme.
     * @param playbackLooper               The looper associated with the media playback thread. Should usually be
     *                                     obtained using {@link com.google.android.exoplayer.ExoPlayer#getPlaybackLooper()}.
     * @param callback                     Performs key and provisioning requests.
     * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
     *                                     to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
     * @param eventHandler                 A handler to use when delivering events to {@code eventListener}. May be
     *                                     null if delivery of events is not required.
     * @param eventListener                A listener of events. May be null if delivery of events is not required.
     * @throws UnsupportedDrmException If the specified DRM scheme is not supported.
     */
    private OfflineDRMSessionManager(UUID uuid, Looper playbackLooper, MediaDrmCallback callback,
                                    HashMap<String, String> optionalKeyRequestParameters, Handler eventHandler,
                                    EventListener eventListener,byte[] keySetId) throws UnsupportedDrmException
    {
        this.uuid = uuid;
        this.callback = callback;
        this.optionalKeyRequestParameters = optionalKeyRequestParameters;
        this.eventHandler = eventHandler;
        this.eventListener = eventListener;
        this.keySetID =keySetId;
        try
        {
            mediaDrm = new MediaDrm(uuid);
        }
        catch (UnsupportedSchemeException e)
        {
            throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME, e);
        }
        catch (Exception e)
        {
            throw new UnsupportedDrmException(UnsupportedDrmException.REASON_INSTANTIATION_ERROR, e);
        }
        mediaDrm.setOnEventListener(new MediaDrmEventListener());
        mediaDrmHandler = new MediaDrmHandler(playbackLooper);
        state = STATE_CLOSED;
    }


    /**
     * Opens the session, possibly asynchronously.
     *
     * @param drmInitData DRM initialization data.
     */
    @Override
    public void open(DrmInitData drmInitData)
    {
        if (++openCount != 1)
        {
            return;
        }
        state = STATE_OPENING;
        openInternal(true);
    }


    private void openInternal(boolean allowProvisioning)
    {
        try
        {
            sessionId = mediaDrm.openSession();
            Log.i(TAG, "+++Creating MediaCrypto");
            mediaCrypto = new MediaCrypto(uuid, sessionId);
            state = STATE_OPENED;
            restoreKeys();
        }
        catch (NotProvisionedException e)
        {
            if (allowProvisioning)
            {
                //postProvisionRequest();
            }
            else
            {
                onError(e);
            }
        }
        catch (Exception e)
        {
            onError(e);
        }
    }

    /**
     * Closes the session.
     */
    @Override
    public void close()
    {
        if (--openCount != 0)
        {
            return;
        }
        state = STATE_CLOSED;
        mediaDrmHandler.removeCallbacksAndMessages(null);
        mediaCrypto = null;
        lastException = null;
        if (sessionId != null)
        {
            mediaDrm.closeSession(sessionId);
            sessionId = null;
        }
    }

    /**
     * Gets the current state of the session.
     *
     * @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING},
     * {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}.
     */
    @Override
    public int getState()
    {
        return state;
    }

    /**
     * Gets a {@link MediaCrypto} for the open session.
     * <p/>
     * This method may be called when the manager is in the following states:
     * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS}
     *
     * @return A {@link MediaCrypto} for the open session.
     * @throws IllegalStateException If called when a session isn't opened.
     */
    @Override
    public MediaCrypto getMediaCrypto()
    {
        if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS)
        {
            throw new IllegalStateException();
        }
        return mediaCrypto;
    }

    /**
     * Whether the session requires a secure decoder for the specified mime type.
     * <p/>
     * Normally this method should return {@link MediaCrypto#requiresSecureDecoderComponent(String)},
     * however in some cases implementations  may wish to modify the return value (i.e. to force a
     * secure decoder even when one is not required).
     * <p/>
     * This method may be called when the manager is in the following states:
     * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS}
     *
     * @param mimeType
     * @return Whether the open session requires a secure decoder for the specified mime type.
     * @throws IllegalStateException If called when a session isn't opened.
     */
    @Override
    public boolean requiresSecureDecoderComponent(String mimeType)
    {
        if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS)
        {
            throw new IllegalStateException();
        }
        return mediaCrypto.requiresSecureDecoderComponent(mimeType);
    }

    /**
     * Gets the cause of the error state.
     * <p/>
     * This method may be called when the manager is in any state.
     *
     * @return An exception if the state is {@link #STATE_ERROR}. Null otherwise.
     */
    @Override
    public Exception getError()
    {
        return state == STATE_ERROR ? lastException : null;
    }


    @SuppressLint("HandlerLeak")
    private class MediaDrmHandler extends Handler
    {

        public MediaDrmHandler(Looper looper)
        {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg)
        {
            if (openCount == 0 || (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS))
            {
                return;
            }
            switch (msg.what)
            {
                case MediaDrm.EVENT_KEY_REQUIRED:
                    restoreKeys();
                    return;
                case MediaDrm.EVENT_KEY_EXPIRED:
                    state = STATE_OPENED;
                    onError(new KeysExpiredException());
                    return;
            }
        }

    }

    private class MediaDrmEventListener implements MediaDrm.OnEventListener
    {

        @Override
        public void onEvent(MediaDrm md, byte[] sessionId, int event, int extra, byte[] data)
        {
            mediaDrmHandler.sendEmptyMessage(event);
        }

    }

    private void onError(final Exception e)
    {
        lastException = e;
        if (eventHandler != null && eventListener != null)
        {
            eventHandler.post(new Runnable()
            {
                @Override
                public void run()
                {
                    eventListener.onDrmSessionManagerError(e);
                }
            });
        }
        if (state != STATE_OPENED_WITH_KEYS)
        {
            state = STATE_ERROR;
        }
    }

    private void restoreKeys()
    {
        if(keySetID ==null)
        {
            return;
        }

        mediaDrm.restoreKeys(sessionId, keySetID);
        state = STATE_OPENED_WITH_KEYS;
    }

}

@Amritpal33 awesome! Thanks for the code samples, this really helped me get going with offline DRM.
As mentioned above, I would also like an official code sample/implementation in the Exoplayer project to achieve this.

Does anyone interested in the same implementation for the ExoPlayer v2 ?

@ChernyshovYuriy we are interested in the same implementation in Exoplayer. Is it possible to download DRM protected DASH stream to be downloaded using exoplayer to playback offline without Internet?

Hi @techguy2 .
The intention of my question was:
"does any one else tried to implement solution provided by Amritpal33 commented on Jul 25 with Exo Player v2".
Sorry if I confused someone, I do not have solution so far and as well.

Hello @ChernyshovYuriy and all

I have implemented it for an offline player reading dash encrypted videos (using mpd descriptor)

Here is how i made it work :

A. Use the two classes from @Amritpal33 (_WidevineKeysFetcher_ and _OfflineDRMSessionManager)_
I've modified _WidevineKeysFetcher_ because using it as a singleton causes issues when using it multiple times. Please see https://github.com/papaanoel/exoplayer-dash-offline/tree/master/app/src/main/java/fr/artestudio/offlinedrm/helper/widewine for modified versions.

B. Then use _WidevineKeysFetcher_ to retrieve licence when needed (I personnaly call it before downloading the video segments)

WidevineKeysFetcher fetcher = new WidevineKeysFetcher(); fetcher.fetchKeys(context, null, "fakeuseragent", dashUrl, new FragmentedMp4Extractor());

Important note here, dashUrl is the url of the "initialisation" parameter in the mpd for example :

<SegmentTemplate timescale="48000" initialization="video50-$RepresentationID$.dash" media="video50-$RepresentationID$-$Number$.m4s" startNumber="1" duration="144000"> </SegmentTemplate>

_WidevineKeysFetcher_ has a callback method private void onKeyResponse(Object response) which gives you the licence key that you will have to store somewhere (database, filesystem) for future use.

C. Once you have called and stored your licence somewhere in your app, you will be able to use it like this

Call the exoplayer activity playerActivity with an extra key PlayerActivity.KEY_SET_ID containing the licence that you have stored :

Intent mpdIntent = new Intent(v.getContext(), PlayerActivity.class) .setData(Uri.parse(le.getUrl())) .putExtra(PlayerActivity.CONTENT_ID_EXTRA, "") .putExtra(PlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_DASH) .putExtra(PlayerActivity.PROVIDER_EXTRA, "") .putExtra(PlayerActivity.KEY_SET_ID, "your_licence_key"); v.getContext().startActivity(mpdIntent);

and modify the private Player.RendererBuilder getRendererBuilder() in playerActivity so that the DashRendererBuilder uses the licence you have stored :

// Internal methods
    private Player.RendererBuilder getRendererBuilder() {
        String userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
        switch (contentType) {
            case Util.TYPE_SS:
                return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString(),
                        new SmoothStreamingTestMediaDrmCallback());
            case Util.TYPE_DASH:
                return new DashRendererBuilder(this, userAgent, contentUri.toString(),
                        new WidevineMediaDrmCallback(true), keySetId);
            case Util.TYPE_HLS:
                return new HlsRendererBuilder(this, userAgent, contentUri.toString());
            case Util.TYPE_OTHER:
                return new ExtractorRendererBuilder(this, userAgent, contentUri);
            default:
                throw new IllegalStateException("Unsupported type: " + contentType);
        }
    }

Last, in DashRendererBuilder, in buildRenderers() method, instead of using

 drmSessionManager = StreamingDrmSessionManager.newWidevineInstance(
                                player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player);

use

drmSessionManager = OfflineDRMSessionManager.newWidevineInstance(player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player, keySetId.getBytes());

This worked for me, hope this will help.

We have just finished our offline Dash+Widevine implementation now and we chose a slightly different method to fetch the init segment. Instead of using a class like the proposed WidevineKeysFetcher we basically just created a separate DashRenderBuilder that uses a configuration of the OfflineDRMSessionManager for storing the DRM keys.

Then we are using an Android Service that sets up a headless instance of Exoplayer using this RenderBuilder and just calls prepare() on the player.
This leads to that Exoplayer internally calls all logic for fetching the init-segment and passing it to the OfflineDRMSessionManager.

Don't know if this is a slightly unorthodox way of solving it but it's an easy setup and seems to work fine for us so far.

I tried the logic provided by @papaanoel and @Amritpal33 but I am getting exceptions in WidevineKeysFetcher class.
what is the "dashUrl" point to in the method signature fetchKeys.
and is there any sample app available where I get the implemetation logic (even a crude is ok).

Hello @ram992,

dashUrl in the method signature fetchKeys(...) is the url of the initialization segment of the video stream.
For example, if you're using a mpd container, considering the following piece of mpd :

<AdaptationSet group="2" contentType="video" par="16:9" minBandwidth="2356000" maxBandwidth="2356000" segmentAlignment="true" width="1280" height="720" mimeType="video/mp4" codecs="avc1.640028" startWithSAP="1">
<!--  Common Encryption  -->
<ContentProtection schemeIdUri="urn:mpeg:dash:mp4" value="cenc" cenc:default_KID=""></ContentProtection>
<!--  PlayReady  -->
<ContentProtection schemeIdUri="" value="MSPR 2.0"></ContentProtection>
<!--  Widevine  -->
<ContentProtection schemeIdUri=""></ContentProtection>
<!--  Marlin  -->
<ContentProtection schemeIdUri="">
<mas:MarlinContentIds>
<mas:MarlinContentId></mas:MarlinContentId>
</mas:MarlinContentIds>
</ContentProtection>
<SegmentTemplate timescale="12800" initialization="init/initSegment-$RepresentationID$.dash" media="video-$RepresentationID$-$Number$.m4s" startNumber="1" duration="38400"></SegmentTemplate>
<Representation id="video=2356000" bandwidth="2356000" scanType="progressive"></Representation>
</AdaptationSet>

the initialization segment is located in <segmentTemplate>

<SegmentTemplate timescale="12800" initialization="init/initSegment-$RepresentationID$.dash" media="video-$RepresentationID$-$Number$.m4s" startNumber="1" duration="38400"></SegmentTemplate>

and thus the initialization segment in this case is init/initSegment-video=2356000.dash

Consequently, dashUrl should be http://<absolute_path_to_your_cdn>/init/initSegment-ideo=2356000.dash and that's what should be passed in the fetchKeys method.

Hope this will help,

The keySetId needs to be stored to restore later. How can this be securely stored ?

so obtain the key from mediaDrm.getKeyRequest, then stored with ? I've seen one method use getSharedPreferences.

"keySetId" is associated Id of the key that had been stored in the underlying native DRM layer of the Android Framework. It is not exact DRM key. So you are secure enough.

What kind of unique identifier could be used for storing the keySetId ? Is there a specific contentid made available ? The code I've seen is using a base64 representation of schemeData.data which is probably no good ?

Thank you @papaanoel for your quick reply I am facing one more issue, (when online and before making the changes you have provided )
In ExoPlayer 1.5.8 the video is playing for 9 or 10 seconds and then going into ideal state with this error

com.google.android.exoplayer.ExoPlaybackException: android.media.MediaCodec$CryptoException: Unknown Error

I read from other thread on github that these kind of issues have been fixed in ExoPlayer-v2.0 but due to some project dependency I have to use 1.5.8.

Do you know why this issue occurs. Currently I am using an HTC device running android 4.4 API-19

@ram992 have you tried playing your mpd file with the ExoPlayer demo app ? Are you experiencing the same issue ? Not sure it has to deal with your Android version, have you tried playing it on another device (running lollipop for example) ?
For ExoPlayer 2, i've not tried it yet, but as far as i know the logic should be the same...

Thank you @papaanoel for taking time to reply
I am using the ExoPlayer Demo app itself, and Some of the example content in the app itself is not working.

I have tried on MarshMellow ( Moto G4 plus )in that after playing for about 10 sec.
I was getting an error saying "request keys have not been loaded"

In both the cases I am testing with online mode.
and changed the liscense proxy to my one (to check my content)

@ram992 did you have a look at this ?
https://github.com/google/ExoPlayer/issues/968

I'm attempting what someone else has done and do a background task.

Your code is great however assuming to know what is in the dash manifest is not workable.

No idea about a downloader yet apart from using a cache data source. The FairPlay SDK has an offline downloader client that ExoPlayer might need.

Outside the custom drm manager I dispatching and listening to the drm loaded even to save this keysetId like

public void onDrmKeysLoaded(byte[] keyResponse) {

    eventLogger.onDrmKeysLoaded(keyResponse);
  }

This needs to be stored possibly with the current requested file as the unique key. there seems to be no such api method to obtain the currently loaded url , even the generated mediasource api has no method.

Is there a way to obtain the file name then I can do possible storage like

public void onDrmKeysLoaded(byte[] keyResponse) {
String keyResponseString = Base64.encodeToString(keyResponse, Base64.DEFAULT);
            SharedPreferences settings = mContext.getSharedPreferences("PREFS_NAME", 0);
            SharedPreferences.Editor editor = settings.edit();
            editor.putString("uniqueidoffilename", keyResponseString);

            editor.apply();
    eventLogger.onDrmKeysLoaded(keyResponse);
  }

This is the best thing I can find unless there is a better way to store this keysetid. The mediadrm should have some kind of persistent storage perhaps.

It has an api method for setting byte arrays after all

mediadrm.setPropertyByteArray(key, value);

Yeah @papaanoel, thanks for pointing out, I am working on this issue. And will get back to you once I get any answer/solution. Thank you

Ok funny story. I think everyone is working on V1 code. I am using the drm session manager from V2.

It is partially there. I get the keysetId and save externally. then set it back on the manager for that file when setting the mediasource like

String offlineKey = Base64.encodeToString(currentUrl.getBytes(), Base64.DEFAULT);
    String keySetId = settings.getString(offlineKey, "");

    if (!"".equals(keySetId)) {
      PersistDrmSessionManager<FrameworkMediaCrypto> manager = (PersistDrmSessionManager<FrameworkMediaCrypto>)drmSessionManager;
      manager.setKetSetId(Base64.decode(keySetId, Base64.DEFAULT));
    }

It's failing however to rebuild due to this annotated code saying State can't be found.

    @Override
    @DrmSession.State
    public final int getState() {
        return state;
    }

Any ideas there ?

@DrmSession.State is an annotation with source retention in the library. If you are seeing the error in a file containing your own DrmSession implementation, you might be able to fix it by removing the annotation in your code. If that doesn't work, could you clarify where exactly you see the error?

@andrewlewis I've made a copy of StreamingDrmSessionManager , renamed it to PersistDrmSessionManager, in the same package in the ExoPlayer demo project. To provide the offline key support. Similar to above.

In Android studio it will fail to rebuild project and I believe when trying to debug it fails to load the app because of it.

Error:(305, 16) error: cannot find symbol class State

There is also this annotation

@DrmSession.State
  private int state;

@danrossi Does it build if you just remove line with the error? The annotation is removed during compilation (its purpose is to allow some compile-time checks) so it won't be present in the class files.

@andrewlewis Yes it's rebuilding now without the annotations no worries.

I'm just using the barebones demo app from the sources and an online Dash file.

Confirming in the emulator on re-run of the online Dash file for now it is obtaining this keysetid and restoring the key with my custom manager. So not requesting a key remotely.

In my log I made this dispatched from the drm manager.

Offline KeysetId [B@bbe5f21 restored

I just need to find a way now to properly obtain the current mediasource url before actual playback so I can configure the manager with the stored keysetid. Right now I've done that a bit dodgy within the mediasource creation method as in the code above.

Is there a proper way also to download and cache the Dash files in player v2 ? The Cache source ?

I see reference to it now in dev v2 like a few hours ago but no idea how to use it. May have to check this out now and start the project again.

An external project and dependancy configured didn't work so stuck using the demo project for now.

https://github.com/google/ExoPlayer/blob/dev-v2/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java

I believe there is a few options for offline playback but not confirmed yet. There is the CacheDataSource and FileDataSource. Some code I've seen for caching dash is

 LeastRecentlyUsedCacheEvictor evictor = new LeastRecentlyUsedCacheEvictor(100 * 1024 * 1024);
        SimpleCache simpleCache = new SimpleCache(new File(getCacheDir(), "media_cache"), evictor);
        DefaultDataSource upstream = defaultMediaDataSourceFactory.createDataSource();
        return new CacheDataSource(simpleCache, upstream, false, false);

Not sure where Dash mediasource fits into this. Not sure if an mpd file can be loaded on FileDataSource.

Hi all,

Did someone try to import OfflineDRMSessionManager from ExoPlayer 1.x to ExoPlayer 2 (change the imports)? I have a problem with @Override methods (open(), close(), ..) which are NOT in that new DrmSessionManager and there are some new methods: acquireSession(), ...

I've created a gist of some files i've pieced together. This probably should go back in. Ideally the session manager should be extendable rather than private methods.

https://gist.github.com/danrossi/495826b5c88948f2aded05574d64e239

@Pepa112 we're working on a new OfflineDrmSessionManager (which will probably end up as a DrmSessionManager which supports offline and streaming, and a different class name)

@erdemguven that would solve everyones needs. But how about a downloader function ? I'm currently trying to work on something right now its not easy. It will replicate the download manager in the FairPlay sdk in some ways. I'm assuming once the files are downloaded all that needs to be done is reference the mpd file with file:// and it will use the FileDataSource

@danrossi unfortunately there is no development for content downloading
currently. After offline drm support, it will follow.

Once you download the files, you might need to modify the mpd to point
content files relatively.

@erdemguven Is there any time estimation for offline support (even as beta)? Is it a few weeks or a few months?

@noamtamim a few weeks.

@erdemguven i am looking forward to offline support. Any progress?

@erenbakac it's going through code review. Should be committed in 1 or 2 weeks.

To clarify in case there's any confusion here: As far as I know the thing being delivered in 1 or 2 weeks is the new DrmSessionManager that will support playback using an offline license, together with logic for acquiring/refreshing/releasing offline licenses. You shouldn't expect a comprehensive solution for offline (i.e. also downloading all of the necessary media chunks) to be provided within that time frame. We don't have an ETA for that as far as I know. Please correct me if I'm wrong!

There is no downloader. I'm trying to figure one out right now. I tried the cache system which was a failure, but now trying to replicate the whole manifest parser downloader.

@ojw28 , you are absolutely right, from client point of view of course.
Offline license is a small part of offline playback. In fact, it is not so hard to implement it based on the current version which is ExoPlayer 2.1.1.
Now, for those how are awaiting for this feature and do not have functionality of the offline playback, are you ready for a challenge to solve to keep downloaded manifest file (keep in mind, it can be any media though - hls, dash, ss) as well as all necessary chunks of media (audio, video, data) locally? More important, because you have a license, you must keep those data secure.
From my own point of view, this is not player responsibility at all to keep media data offline.

Hi,
I'm able to play the file in offline in ExoPlayer V2.
I have made the changes in StreamingDrmSessionManager.java itself.

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

  }

This has been implemented in the change 9d5c750fe914cc7c51f8afa49bb113121352510f

@edwinwong I took your changes and when i run the app, throwing "Media requires a DrmSessionManager" exception which is shown below

01-20 17:40:56.179 22115-22359/com.google.android.exoplayer2.demo E/ExoPlayerImplInternal: Renderer error.
                                                                                           com.google.android.exoplayer2.ExoPlaybackException
                                                                                               at com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.onInputFormatChanged(MediaCodecRenderer.java:729)
                                                                                               at com.google.android.exoplayer2.video.MediaCodecVideoRenderer.onInputFormatChanged(MediaCodecVideoRenderer.java:325)
                                                                                               at com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.readFormat(MediaCodecRenderer.java:496)
                                                                                               at com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.render(MediaCodecRenderer.java:479)
                                                                                               at com.google.android.exoplayer2.ExoPlayerImplInternal.doSomeWork(ExoPlayerImplInternal.java:479)
                                                                                               at com.google.android.exoplayer2.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:315)
                                                                                               at android.os.Handler.dispatchMessage(Handler.java:98)
                                                                                               at android.os.Looper.loop(Looper.java:146)
                                                                                               at android.os.HandlerThread.run(HandlerThread.java:61)
                                                                                               at com.google.android.exoplayer2.util.PriorityHandlerThread.run(PriorityHandlerThread.java:40)
                                                                                            Caused by: java.lang.IllegalStateException: Media requires a DrmSessionManager
                                                                                               at com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.onInputFormatChanged(MediaCodecRenderer.java:729) 
                                                                                               at com.google.android.exoplayer2.video.MediaCodecVideoRenderer.onInputFormatChanged(MediaCodecVideoRenderer.java:325) 
                                                                                               at com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.readFormat(MediaCodecRenderer.java:496) 
                                                                                               at com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.render(MediaCodecRenderer.java:479) 
                                                                                               at com.google.android.exoplayer2.ExoPlayerImplInternal.doSomeWork(ExoPlayerImplInternal.java:479) 
                                                                                               at com.google.android.exoplayer2.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:315) 
                                                                                               at android.os.Handler.dispatchMessage(Handler.java:98) 
                                                                                               at android.os.Looper.loop(Looper.java:146) 
                                                                                               at android.os.HandlerThread.run(HandlerThread.java:61) 
                                                                                               at com.google.android.exoplayer2.util.PriorityHandlerThread.run(PriorityHandlerThread.java:40) 

@ojw28
Do you think the "new DrmSessionManager" (described above) will be compliant to Hollywood studio requirements?
@ChernyshovYuriy
What is your recommendation to keep media data offline, especially if it comes to Hollywood studio content?

What does "Hollywood studio" means?
Regarding our question - you need store manifest and all media data (chunks with video/audio) locally, then you need to use local server to be implemented which will feed you back all these data upon ExoPlayer request.
This is very complicated task and I am under NDA to discuss details, but this is doable.

@chris-schu - The DRM implementation (and enforcement of the license policy) is part of the platform, not part of ExoPlayer. DrmSessionManager just sits on top of the relevant platform APIs. So your question is really whether the underlying platform support is sufficient.

If you're using Widevine, L1 is _typically_ sufficient for HD and L3 is _typically_ sufficient for SD for most studios. Whether that's true in your case really depends on the specifics of the contracts you have, and so we're not in a position to provide concrete advice for your case.

Dear All,

I am using exoplayer com.google.android.exoplayer:exoplayer:r2.0.3 in android to play videos.
DRM protect videos .mpd are playing fine with online.
I am able download video protected files(.mpd).
But not able to play .mpd files with offline.
Please suggest or give sample code for exoplayer offline drm for android code.

@chandrasekhararao Offline DRM support was added in ExoPlayer v2.2.0. You need to upgrade.

In the meantime, v2.3.0 was released, so you better jump directly to it.

Hello, can anyone here please add a sample on how to achieve offline drm playback to the demo app?

I have a secured widevine .mpd url to protected content and a licence url generated on my server, online playback works flawlessly, now, I'm requested to download the content (for now only the video, but I'll need audio, subtitles, etc) and store them inside the device alongside with the offline license for offline playback, here is where I'm stuck due to the lack of samples.

I've managed to download the mp4 file and store it on the device's root folder, from here, I'm stuck.

Thank you very much!

@nosmirck

I can't really provide a sample... but I got it working with 2.3.1 exoplayer (the download utils seem to have changed in 2.4.0) and will point you in the right direction

basically need to save the mpd file and the mp4 files on the device (you could just manually do this just to see if it will work, parsing an xml and downloading files is just like any other file). then you have to implement a track selector so that it will not play any tracks you did not download (or download all tracks).

you need to also store the keyID somewhere (probably base64 encode it into sqllite as a string... thats what i did) that you get once you fetch the license. Then you have to restore that key and set the mode of your DefaultDrmSessionManager to MODE_PLAYBACK with that keyId and inject that into the simpleexoplayer

Was this page helpful?
0 / 5 - 0 ratings