diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 22af39517a..9053f8990a 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.decoder.DecoderCounters; -import com.google.android.exoplayer2.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.metadata.id3.ApicFrame; @@ -55,7 +55,7 @@ import java.util.Locale; */ /* package */ final class EventLogger implements ExoPlayer.EventListener, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, - ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener, + ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener, MetadataRenderer.Output { private static final String TAG = "EventLogger"; @@ -279,13 +279,23 @@ import java.util.Locale; // Do nothing. } - // StreamingDrmSessionManager.EventListener + // DefaultDrmSessionManager.EventListener @Override public void onDrmSessionManagerError(Exception e) { printInternalError("drmSessionManagerError", e); } + @Override + public void onDrmKeysRestored() { + Log.d(TAG, "drmKeysRestored [" + getSessionTimeString() + "]"); + } + + @Override + public void onDrmKeysRemoved() { + Log.d(TAG, "drmKeysRemoved [" + getSessionTimeString() + "]"); + } + @Override public void onDrmKeysLoaded() { Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 243fcadce0..2d7c8189a2 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -36,11 +36,11 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; -import com.google.android.exoplayer2.drm.StreamingDrmSessionManager; import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; @@ -358,7 +358,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, buildHttpDataSourceFactory(false), keyRequestProperties); - return new StreamingDrmSessionManager<>(uuid, + return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger); } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java new file mode 100644 index 0000000000..0342e37bd6 --- /dev/null +++ b/library/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.exoplayer2.drm; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; + +import android.test.InstrumentationTestCase; +import android.test.MoreAsserts; +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; +import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.source.dash.manifest.InbandEventStream; +import com.google.android.exoplayer2.source.dash.manifest.Period; +import com.google.android.exoplayer2.source.dash.manifest.Representation; +import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.HttpDataSource; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import org.mockito.Mock; + +/** + * Tests {@link OfflineLicenseHelper}. + */ +public class OfflineLicenseHelperTest extends InstrumentationTestCase { + + private OfflineLicenseHelper offlineLicenseHelper; + @Mock private HttpDataSource httpDataSource; + @Mock private MediaDrmCallback mediaDrmCallback; + @Mock private ExoMediaDrm mediaDrm; + + @Override + protected void setUp() throws Exception { + TestUtil.setUpMockito(this); + + when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3}); + + offlineLicenseHelper = new OfflineLicenseHelper<>(mediaDrm, mediaDrmCallback, null); + } + + @Override + protected void tearDown() throws Exception { + offlineLicenseHelper.releaseResources(); + } + + public void testDownloadRenewReleaseKey() throws Exception { + DashManifest manifest = newDashManifestWithAllElements(); + setStubLicenseAndPlaybackDurationValues(1000, 200); + + byte[] keySetId = {2, 5, 8}; + setStubKeySetId(keySetId); + + byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest); + + assertOfflineLicenseKeySetIdEqual(keySetId, offlineLicenseKeySetId); + + byte[] keySetId2 = {6, 7, 0, 1, 4}; + setStubKeySetId(keySetId2); + + byte[] offlineLicenseKeySetId2 = offlineLicenseHelper.renew(offlineLicenseKeySetId); + + assertOfflineLicenseKeySetIdEqual(keySetId2, offlineLicenseKeySetId2); + + offlineLicenseHelper.release(offlineLicenseKeySetId2); + } + + public void testDownloadFailsIfThereIsNoInitData() throws Exception { + setDefaultStubValues(); + DashManifest manifest = + newDashManifest(newPeriods(newAdaptationSets(newRepresentations(null /*no init data*/)))); + + byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest); + + assertNull(offlineLicenseKeySetId); + } + + public void testDownloadFailsIfThereIsNoRepresentation() throws Exception { + setDefaultStubValues(); + DashManifest manifest = newDashManifest(newPeriods(newAdaptationSets(/*no representation*/))); + + byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest); + + assertNull(offlineLicenseKeySetId); + } + + public void testDownloadFailsIfThereIsNoAdaptationSet() throws Exception { + setDefaultStubValues(); + DashManifest manifest = newDashManifest(newPeriods(/*no adaptation set*/)); + + byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest); + + assertNull(offlineLicenseKeySetId); + } + + public void testDownloadFailsIfThereIsNoPeriod() throws Exception { + setDefaultStubValues(); + DashManifest manifest = newDashManifest(/*no periods*/); + + byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest); + + assertNull(offlineLicenseKeySetId); + } + + public void testDownloadFailsIfNoKeySetIdIsReturned() throws Exception { + setStubLicenseAndPlaybackDurationValues(1000, 200); + DashManifest manifest = newDashManifestWithAllElements(); + + byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest); + + assertNull(offlineLicenseKeySetId); + } + + public void testDownloadDoesNotFailIfDurationNotAvailable() throws Exception { + setDefaultStubKeySetId(); + DashManifest manifest = newDashManifestWithAllElements(); + + byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest); + + assertNotNull(offlineLicenseKeySetId); + } + + public void testGetLicenseDurationRemainingSec() throws Exception { + long licenseDuration = 1000; + int playbackDuration = 200; + setStubLicenseAndPlaybackDurationValues(licenseDuration, playbackDuration); + setDefaultStubKeySetId(); + DashManifest manifest = newDashManifestWithAllElements(); + + byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest); + + Pair licenseDurationRemainingSec = offlineLicenseHelper + .getLicenseDurationRemainingSec(offlineLicenseKeySetId); + + assertEquals(licenseDuration, (long) licenseDurationRemainingSec.first); + assertEquals(playbackDuration, (long) licenseDurationRemainingSec.second); + } + + public void testGetLicenseDurationRemainingSecExpiredLicense() throws Exception { + long licenseDuration = 0; + int playbackDuration = 0; + setStubLicenseAndPlaybackDurationValues(licenseDuration, playbackDuration); + setDefaultStubKeySetId(); + DashManifest manifest = newDashManifestWithAllElements(); + + byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest); + + Pair licenseDurationRemainingSec = offlineLicenseHelper + .getLicenseDurationRemainingSec(offlineLicenseKeySetId); + + assertEquals(licenseDuration, (long) licenseDurationRemainingSec.first); + assertEquals(playbackDuration, (long) licenseDurationRemainingSec.second); + } + + private void setDefaultStubValues() + throws android.media.NotProvisionedException, android.media.DeniedByServerException { + setDefaultStubKeySetId(); + setStubLicenseAndPlaybackDurationValues(1000, 200); + } + + private void setDefaultStubKeySetId() + throws android.media.NotProvisionedException, android.media.DeniedByServerException { + setStubKeySetId(new byte[] {2, 5, 8}); + } + + private void setStubKeySetId(byte[] keySetId) + throws android.media.NotProvisionedException, android.media.DeniedByServerException { + when(mediaDrm.provideKeyResponse(any(byte[].class), any(byte[].class))).thenReturn(keySetId); + } + + private static void assertOfflineLicenseKeySetIdEqual( + byte[] expectedKeySetId, byte[] actualKeySetId) throws Exception { + assertNotNull(actualKeySetId); + MoreAsserts.assertEquals(expectedKeySetId, actualKeySetId); + } + + private void setStubLicenseAndPlaybackDurationValues(long licenseDuration, + long playbackDuration) { + HashMap keyStatus = new HashMap<>(); + keyStatus.put(WidevineUtil.PROPERTY_LICENSE_DURATION_REMAINING, + String.valueOf(licenseDuration)); + keyStatus.put(WidevineUtil.PROPERTY_PLAYBACK_DURATION_REMAINING, + String.valueOf(playbackDuration)); + when(mediaDrm.queryKeyStatus(any(byte[].class))).thenReturn(keyStatus); + } + + private static DashManifest newDashManifestWithAllElements() { + return newDashManifest(newPeriods(newAdaptationSets(newRepresentations(newDrmInitData())))); + } + + private static DashManifest newDashManifest(Period... periods) { + return new DashManifest(0, 0, 0, false, 0, 0, 0, null, null, Arrays.asList(periods)); + } + + private static Period newPeriods(AdaptationSet... adaptationSets) { + return new Period("", 0, Arrays.asList(adaptationSets)); + } + + private static AdaptationSet newAdaptationSets(Representation... representations) { + return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), + Collections.emptyList()); + } + + private static Representation newRepresentations(DrmInitData drmInitData) { + Format format = Format.createVideoSampleFormat("", "", "", 0, 0, 0, 0, 0, null, drmInitData); + return Representation.newInstance("", 0, format, "", new SingleSegmentBase()); + } + + private static DrmInitData newDrmInitData() { + return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType", + new byte[]{1, 4, 7, 0, 3, 6})); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/StreamingDrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java similarity index 63% rename from library/src/main/java/com/google/android/exoplayer2/drm/StreamingDrmSessionManager.java rename to library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 4e4845c70b..6eb70428d5 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/StreamingDrmSessionManager.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -24,7 +24,10 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import android.support.annotation.IntDef; import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; @@ -33,18 +36,21 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.HashMap; +import java.util.Map; import java.util.UUID; /** - * A {@link DrmSessionManager} that supports streaming playbacks using {@link MediaDrm}. + * A {@link DrmSessionManager} that supports playbacks using {@link MediaDrm}. */ @TargetApi(18) -public class StreamingDrmSessionManager implements DrmSessionManager, +public class DefaultDrmSessionManager implements DrmSessionManager, DrmSession { /** - * Listener of {@link StreamingDrmSessionManager} events. + * Listener of {@link DefaultDrmSessionManager} events. */ public interface EventListener { @@ -60,6 +66,16 @@ public class StreamingDrmSessionManager implements Drm */ void onDrmSessionManagerError(Exception e); + /** + * Called each time offline keys are restored. + */ + void onDrmKeysRestored(); + + /** + * Called each time offline keys are removed. + */ + void onDrmKeysRemoved(); + } /** @@ -67,9 +83,32 @@ public class StreamingDrmSessionManager implements Drm */ public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData"; + /** Determines the action to be done after a session acquired. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({MODE_PLAYBACK, MODE_QUERY, MODE_DOWNLOAD, MODE_RELEASE}) + public @interface Mode {} + /** + * Loads and refreshes (if necessary) a license for playback. Supports streaming and offline + * licenses. + */ + public static final int MODE_PLAYBACK = 0; + /** + * Restores an offline license to allow its status to be queried. If the offline license is + * expired sets state to {@link #STATE_ERROR}. + */ + public static final int MODE_QUERY = 1; + /** Downloads an offline license or renews an existing one. */ + public static final int MODE_DOWNLOAD = 2; + /** Releases an existing offline license. */ + public static final int MODE_RELEASE = 3; + + private static final String TAG = "OfflineDrmSessionMngr"; + private static final int MSG_PROVISION = 0; private static final int MSG_KEYS = 1; + private static final int MAX_LICENSE_DURATION_TO_RENEW = 60; + private final Handler eventHandler; private final EventListener eventListener; private final ExoMediaDrm mediaDrm; @@ -85,14 +124,17 @@ public class StreamingDrmSessionManager implements Drm private HandlerThread requestHandlerThread; private Handler postRequestHandler; + private int mode; private int openCount; private boolean provisioningInProgress; @DrmSession.State private int state; private T mediaCrypto; - private Exception lastException; - private SchemeData schemeData; + private DrmSessionException lastException; + private byte[] schemeInitData; + private String schemeMimeType; private byte[] sessionId; + private byte[] offlineLicenseKeySetId; /** * Instantiates a new instance using the Widevine scheme. @@ -105,7 +147,7 @@ public class StreamingDrmSessionManager implements Drm * @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 StreamingDrmSessionManager newWidevineInstance( + public static DefaultDrmSessionManager newWidevineInstance( MediaDrmCallback callback, HashMap optionalKeyRequestParameters, Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters, @@ -125,7 +167,7 @@ public class StreamingDrmSessionManager implements Drm * @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 StreamingDrmSessionManager newPlayReadyInstance( + public static DefaultDrmSessionManager newPlayReadyInstance( MediaDrmCallback callback, String customData, Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { HashMap optionalKeyRequestParameters; @@ -151,10 +193,10 @@ public class StreamingDrmSessionManager implements Drm * @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 StreamingDrmSessionManager newFrameworkInstance( + public static DefaultDrmSessionManager newFrameworkInstance( UUID uuid, MediaDrmCallback callback, HashMap optionalKeyRequestParameters, Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { - return new StreamingDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback, + return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback, optionalKeyRequestParameters, eventHandler, eventListener); } @@ -168,7 +210,7 @@ public class StreamingDrmSessionManager implements Drm * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - public StreamingDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, + public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, HashMap optionalKeyRequestParameters, Handler eventHandler, EventListener eventListener) { this.uuid = uuid; @@ -179,6 +221,7 @@ public class StreamingDrmSessionManager implements Drm this.eventListener = eventListener; mediaDrm.setOnEventListener(new MediaDrmEventListener()); state = STATE_CLOSED; + mode = MODE_PLAYBACK; } /** @@ -229,6 +272,35 @@ public class StreamingDrmSessionManager implements Drm mediaDrm.setPropertyByteArray(key, value); } + /** + * Sets the mode, which determines the role of sessions acquired from the instance. This must be + * called before {@link #acquireSession(Looper, DrmInitData)} is called. + * + *

By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when + * required. + * + *

{@code mode} must be one of these: + *

  • {@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is + * requested otherwise the offline license is restored. + *
  • {@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license + * is restored. + *
  • {@link #MODE_DOWNLOAD}: If {@code offlineLicenseKeySetId} is null, an offline license is + * requested otherwise the offline license is renewed. + *
  • {@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline license + * is released. + * + * @param mode The mode to be set. + * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode. + */ + public void setMode(@Mode int mode, byte[] offlineLicenseKeySetId) { + Assertions.checkState(openCount == 0); + if (mode == MODE_QUERY || mode == MODE_RELEASE) { + Assertions.checkNotNull(offlineLicenseKeySetId); + } + this.mode = mode; + this.offlineLicenseKeySetId = offlineLicenseKeySetId; + } + // DrmSessionManager implementation. @Override @@ -248,18 +320,22 @@ public class StreamingDrmSessionManager implements Drm requestHandlerThread.start(); postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); - schemeData = drmInitData.get(uuid); - if (schemeData == null) { - onError(new IllegalStateException("Media does not support uuid: " + uuid)); - return this; - } - if (Util.SDK_INT < 21) { - // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. - byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeData.data, C.WIDEVINE_UUID); - if (psshData == null) { - // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. - } else { - schemeData = new SchemeData(C.WIDEVINE_UUID, schemeData.mimeType, psshData); + if (offlineLicenseKeySetId == null) { + SchemeData schemeData = drmInitData.get(uuid); + if (schemeData == null) { + onError(new IllegalStateException("Media does not support uuid: " + uuid)); + return this; + } + schemeInitData = schemeData.data; + schemeMimeType = schemeData.mimeType; + if (Util.SDK_INT < 21) { + // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. + byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, C.WIDEVINE_UUID); + if (psshData == null) { + // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. + } else { + schemeInitData = psshData; + } } } state = STATE_OPENING; @@ -280,7 +356,8 @@ public class StreamingDrmSessionManager implements Drm postRequestHandler = null; requestHandlerThread.quit(); requestHandlerThread = null; - schemeData = null; + schemeInitData = null; + schemeMimeType = null; mediaCrypto = null; lastException = null; if (sessionId != null) { @@ -314,10 +391,25 @@ public class StreamingDrmSessionManager implements Drm } @Override - public final Exception getError() { + public final DrmSessionException getError() { return state == STATE_ERROR ? lastException : null; } + @Override + public Map queryKeyStatus() { + // User may call this method rightfully even if state == STATE_ERROR. So only check if there is + // a sessionId + if (sessionId == null) { + throw new IllegalStateException(); + } + return mediaDrm.queryKeyStatus(sessionId); + } + + @Override + public byte[] getOfflineLicenseKeySetId() { + return offlineLicenseKeySetId; + } + // Internal methods. private void openInternal(boolean allowProvisioning) { @@ -325,7 +417,7 @@ public class StreamingDrmSessionManager implements Drm sessionId = mediaDrm.openSession(); mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId); state = STATE_OPENED; - postKeyRequest(); + doLicense(); } catch (NotProvisionedException e) { if (allowProvisioning) { postProvisionRequest(); @@ -363,20 +455,87 @@ public class StreamingDrmSessionManager implements Drm if (state == STATE_OPENING) { openInternal(false); } else { - postKeyRequest(); + doLicense(); } } catch (DeniedByServerException e) { onError(e); } } - private void postKeyRequest() { + private void doLicense() { + switch (mode) { + case MODE_PLAYBACK: + case MODE_QUERY: + if (offlineLicenseKeySetId == null) { + postKeyRequest(sessionId, MediaDrm.KEY_TYPE_STREAMING); + } else { + if (restoreKeys()) { + long licenseDurationRemainingSec = getLicenseDurationRemainingSec(); + if (mode == MODE_PLAYBACK + && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) { + Log.d(TAG, "Offline license has expired or will expire soon. " + + "Remaining seconds: " + licenseDurationRemainingSec); + postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE); + } else if (licenseDurationRemainingSec <= 0) { + onError(new KeysExpiredException()); + } else { + state = STATE_OPENED_WITH_KEYS; + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDrmKeysRestored(); + } + }); + } + } + } + } + break; + case MODE_DOWNLOAD: + if (offlineLicenseKeySetId == null) { + postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE); + } else { + // Renew + if (restoreKeys()) { + postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE); + } + } + break; + case MODE_RELEASE: + if (restoreKeys()) { + postKeyRequest(offlineLicenseKeySetId, MediaDrm.KEY_TYPE_RELEASE); + } + break; + } + } + + private boolean restoreKeys() { + try { + mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId); + return true; + } catch (Exception e) { + Log.e(TAG, "Error trying to restore Widevine keys.", e); + onError(e); + } + return false; + } + + private long getLicenseDurationRemainingSec() { + if (!C.WIDEVINE_UUID.equals(uuid)) { + return Long.MAX_VALUE; + } + Pair pair = WidevineUtil.getLicenseDurationRemainingSec(this); + return Math.min(pair.first, pair.second); + } + + private void postKeyRequest(byte[] scope, int keyType) { KeyRequest keyRequest; try { - keyRequest = mediaDrm.getKeyRequest(sessionId, schemeData.data, schemeData.mimeType, - MediaDrm.KEY_TYPE_STREAMING, optionalKeyRequestParameters); + keyRequest = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, keyType, + optionalKeyRequestParameters); postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget(); - } catch (NotProvisionedException e) { + } catch (Exception e) { onKeysError(e); } } @@ -393,15 +552,32 @@ public class StreamingDrmSessionManager implements Drm } try { - mediaDrm.provideKeyResponse(sessionId, (byte[]) response); - state = STATE_OPENED_WITH_KEYS; - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrmKeysLoaded(); - } - }); + if (mode == MODE_RELEASE) { + mediaDrm.provideKeyResponse(offlineLicenseKeySetId, (byte[]) response); + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDrmKeysRemoved(); + } + }); + } + } else { + byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response); + if (offlineLicenseKeySetId != null && (keySetId == null || keySetId.length == 0)) { + // This means that the keySetId is unchanged. + } else { + offlineLicenseKeySetId = keySetId; + } + 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); @@ -417,7 +593,7 @@ public class StreamingDrmSessionManager implements Drm } private void onError(final Exception e) { - lastException = e; + lastException = new DrmSessionException(e); if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override @@ -446,11 +622,16 @@ public class StreamingDrmSessionManager implements Drm } switch (msg.what) { case MediaDrm.EVENT_KEY_REQUIRED: - postKeyRequest(); + doLicense(); break; case MediaDrm.EVENT_KEY_EXPIRED: - state = STATE_OPENED; - onError(new KeysExpiredException()); + // When an already expired key is loaded MediaDrm sends this event immediately. Ignore + // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still + // waiting for key response. + if (state == STATE_OPENED_WITH_KEYS) { + state = STATE_OPENED; + onError(new KeysExpiredException()); + } break; case MediaDrm.EVENT_PROVISION_REQUIRED: state = STATE_OPENED; @@ -466,7 +647,9 @@ public class StreamingDrmSessionManager implements Drm @Override public void onEvent(ExoMediaDrm md, byte[] sessionId, int event, int extra, byte[] data) { - mediaDrmHandler.sendEmptyMessage(event); + if (mode == MODE_PLAYBACK) { + mediaDrmHandler.sendEmptyMessage(event); + } } } diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index 6f84395072..4d64187a8b 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -16,9 +16,11 @@ package com.google.android.exoplayer2.drm; import android.annotation.TargetApi; +import android.media.MediaDrm; import android.support.annotation.IntDef; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Map; /** * A DRM session. @@ -26,6 +28,15 @@ import java.lang.annotation.RetentionPolicy; @TargetApi(16) public interface DrmSession { + /** Wraps the exception which is the cause of the error state. */ + class DrmSessionException extends Exception { + + DrmSessionException(Exception e) { + super(e); + } + + } + /** * The state of the DRM session. */ @@ -96,6 +107,26 @@ public interface DrmSession { * * @return An exception if the state is {@link #STATE_ERROR}. Null otherwise. */ - Exception getError(); + DrmSessionException getError(); + + /** + * Returns an informative description of the key status for the session. The status is in the form + * of {name, value} pairs. + * + *

    Since DRM license policies vary by vendor, the specific status field names are determined by + * each DRM vendor. Refer to your DRM provider documentation for definitions of the field names + * for a particular DRM engine plugin. + * + * @return A map of key status. + * @throws IllegalStateException If called when the session isn't opened. + * @see MediaDrm#queryKeyStatus(byte[]) + */ + Map queryKeyStatus(); + + /** + * Returns the key set id of the offline license loaded into this session, if there is one. Null + * otherwise. + */ + byte[] getOfflineLicenseKeySetId(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java new file mode 100644 index 0000000000..12ddc6da1b --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.exoplayer2.drm; + +import android.media.MediaDrm; +import android.net.Uri; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.EventListener; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode; +import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; +import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper; +import com.google.android.exoplayer2.source.chunk.InitializationChunk; +import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; +import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; +import com.google.android.exoplayer2.source.dash.manifest.Period; +import com.google.android.exoplayer2.source.dash.manifest.RangedUri; +import com.google.android.exoplayer2.source.dash.manifest.Representation; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSourceInputStream; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; +import java.io.IOException; +import java.util.HashMap; + +/** + * Helper class to download, renew and release offline licenses. It utilizes {@link + * DefaultDrmSessionManager}. + */ +public final class OfflineLicenseHelper { + + private final ConditionVariable conditionVariable; + private final DefaultDrmSessionManager drmSessionManager; + private final HandlerThread handlerThread; + + /** + * Helper method to download a DASH manifest. + * + * @param dataSource The {@link HttpDataSource} from which the manifest should be read. + * @param manifestUriString The URI of the manifest to be read. + * @return An instance of {@link DashManifest}. + * @throws IOException If an error occurs reading data from the stream. + * @see DashManifestParser + */ + public static DashManifest downloadManifest(HttpDataSource dataSource, String manifestUriString) + throws IOException { + DataSourceInputStream inputStream = new DataSourceInputStream( + dataSource, new DataSpec(Uri.parse(manifestUriString))); + inputStream.open(); + DashManifestParser parser = new DashManifestParser(); + return parser.parse(dataSource.getUri(), inputStream); + } + + /** + * Instantiates a new instance which uses Widevine CDM. Call {@link #releaseResources()} when + * you're done with the helper instance. + * + * @param licenseUrl The default license URL. + * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. + * @return A new instance which uses Widevine CDM. + * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be + * instantiated. + */ + public static OfflineLicenseHelper newWidevineInstance( + String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException { + return newWidevineInstance( + new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory, null), null); + } + + /** + * Instantiates a new instance which uses Widevine CDM. Call {@link #releaseResources()} when + * you're done with the helper instance. + * + * @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. + * @return A new instance which uses Widevine CDM. + * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be + * instantiated. + * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm, + * MediaDrmCallback, HashMap, Handler, EventListener) + */ + public static OfflineLicenseHelper newWidevineInstance( + MediaDrmCallback callback, HashMap optionalKeyRequestParameters) + throws UnsupportedDrmException { + return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), callback, + optionalKeyRequestParameters); + } + + /** + * Constructs an instance. Call {@link #releaseResources()} when you're done with it. + * + * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. + * @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. + * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm, + * MediaDrmCallback, HashMap, Handler, EventListener) + */ + public OfflineLicenseHelper(ExoMediaDrm mediaDrm, MediaDrmCallback callback, + HashMap optionalKeyRequestParameters) { + handlerThread = new HandlerThread("OfflineLicenseHelper"); + handlerThread.start(); + + conditionVariable = new ConditionVariable(); + EventListener eventListener = new EventListener() { + @Override + public void onDrmKeysLoaded() { + conditionVariable.open(); + } + + @Override + public void onDrmSessionManagerError(Exception e) { + conditionVariable.open(); + } + + @Override + public void onDrmKeysRestored() { + conditionVariable.open(); + } + + @Override + public void onDrmKeysRemoved() { + conditionVariable.open(); + } + }; + drmSessionManager = new DefaultDrmSessionManager<>(C.WIDEVINE_UUID, mediaDrm, callback, + optionalKeyRequestParameters, new Handler(handlerThread.getLooper()), eventListener); + } + + /** Releases the used resources. */ + public void releaseResources() { + handlerThread.quit(); + } + + /** + * Downloads an offline license. + * + * @param dataSource The {@link HttpDataSource} to be used for download. + * @param manifestUriString The URI of the manifest to be read. + * @return The downloaded offline license key set id. + * @throws IOException If an error occurs reading data from the stream. + * @throws InterruptedException If the thread has been interrupted. + * @throws DrmSessionException Thrown when there is an error during DRM session. + */ + public byte[] download(HttpDataSource dataSource, String manifestUriString) + throws IOException, InterruptedException, DrmSessionException { + return download(dataSource, downloadManifest(dataSource, manifestUriString)); + } + + /** + * Downloads an offline license. + * + * @param dataSource The {@link HttpDataSource} to be used for download. + * @param dashManifest The {@link DashManifest} of the DASH content. + * @return The downloaded offline license key set id. + * @throws IOException If an error occurs reading data from the stream. + * @throws InterruptedException If the thread has been interrupted. + * @throws DrmSessionException Thrown when there is an error during DRM session. + */ + public byte[] download(HttpDataSource dataSource, DashManifest dashManifest) + throws IOException, InterruptedException, DrmSessionException { + // Get DrmInitData + // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, + // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. + if (dashManifest.getPeriodCount() < 1) { + return null; + } + Period period = dashManifest.getPeriod(0); + int adaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_VIDEO); + if (adaptationSetIndex == C.INDEX_UNSET) { + adaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_AUDIO); + if (adaptationSetIndex == C.INDEX_UNSET) { + return null; + } + } + AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex); + if (adaptationSet.representations.isEmpty()) { + return null; + } + Representation representation = adaptationSet.representations.get(0); + DrmInitData drmInitData = representation.format.drmInitData; + if (drmInitData == null) { + InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation); + if (initializationChunk == null) { + return null; + } + Format sampleFormat = initializationChunk.getSampleFormat(); + if (sampleFormat != null) { + drmInitData = sampleFormat.drmInitData; + } + if (drmInitData == null) { + return null; + } + } + blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, null, drmInitData); + return drmSessionManager.getOfflineLicenseKeySetId(); + } + + /** + * Renews an offline license. + * + * @param offlineLicenseKeySetId The key set id of the license to be renewed. + * @return Renewed offline license key set id. + * @throws DrmSessionException Thrown when there is an error during DRM session. + */ + public byte[] renew(byte[] offlineLicenseKeySetId) throws DrmSessionException { + Assertions.checkNotNull(offlineLicenseKeySetId); + blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, offlineLicenseKeySetId, null); + return drmSessionManager.getOfflineLicenseKeySetId(); + } + + /** + * Releases an offline license. + * + * @param offlineLicenseKeySetId The key set id of the license to be released. + * @throws DrmSessionException Thrown when there is an error during DRM session. + */ + public void release(byte[] offlineLicenseKeySetId) throws DrmSessionException { + Assertions.checkNotNull(offlineLicenseKeySetId); + blockingKeyRequest(DefaultDrmSessionManager.MODE_RELEASE, offlineLicenseKeySetId, null); + } + + /** + * Returns license and playback durations remaining in seconds of the given offline license. + * + * @param offlineLicenseKeySetId The key set id of the license. + */ + public Pair getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId) + throws DrmSessionException { + Assertions.checkNotNull(offlineLicenseKeySetId); + DrmSession session = openBlockingKeyRequest(DefaultDrmSessionManager.MODE_QUERY, + offlineLicenseKeySetId, null); + Pair licenseDurationRemainingSec = + WidevineUtil.getLicenseDurationRemainingSec(drmSessionManager); + drmSessionManager.releaseSession(session); + return licenseDurationRemainingSec; + } + + private void blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId, + DrmInitData drmInitData) throws DrmSessionException { + DrmSession session = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId, + drmInitData); + DrmSessionException error = session.getError(); + if (error != null) { + throw error; + } + drmSessionManager.releaseSession(session); + } + + private DrmSession openBlockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId, + DrmInitData drmInitData) { + drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId); + conditionVariable.close(); + DrmSession session = drmSessionManager.acquireSession(handlerThread.getLooper(), + drmInitData); + // Block current thread until key loading is finished + conditionVariable.block(); + return session; + } + + private static InitializationChunk loadInitializationChunk(final DataSource dataSource, + final Representation representation) throws IOException, InterruptedException { + RangedUri rangedUri = representation.getInitializationUri(); + if (rangedUri == null) { + return null; + } + DataSpec dataSpec = new DataSpec(rangedUri.resolveUri(representation.baseUrl), rangedUri.start, + rangedUri.length, representation.getCacheKey()); + InitializationChunk initializationChunk = new InitializationChunk(dataSource, dataSpec, + representation.format, C.SELECTION_REASON_UNKNOWN, null /* trackSelectionData */, + newWrappedExtractor(representation.format)); + initializationChunk.load(); + return initializationChunk; + } + + private static ChunkExtractorWrapper newWrappedExtractor(final Format format) { + final String mimeType = format.containerMimeType; + final boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM) + || mimeType.startsWith(MimeTypes.AUDIO_WEBM); + final Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor(); + return new ChunkExtractorWrapper(extractor, format, false /* preferManifestDrmInitData */, + false /* resendFormatOnInit */); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java b/library/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java new file mode 100644 index 0000000000..fc80cfb6fb --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.drm; + +import android.util.Pair; +import com.google.android.exoplayer2.C; +import java.util.Map; + +/** + * Utility methods for Widevine. + */ +public final class WidevineUtil { + + /** Widevine specific key status field name for the remaining license duration, in seconds. */ + public static final String PROPERTY_LICENSE_DURATION_REMAINING = "LicenseDurationRemaining"; + /** Widevine specific key status field name for the remaining playback duration, in seconds. */ + public static final String PROPERTY_PLAYBACK_DURATION_REMAINING = "PlaybackDurationRemaining"; + + private WidevineUtil() {} + + /** + * Returns license and playback durations remaining in seconds. + * + * @return A {@link Pair} consisting of the remaining license and playback durations in seconds. + * @throws IllegalStateException If called when a session isn't opened. + * @param drmSession + */ + public static Pair getLicenseDurationRemainingSec(DrmSession drmSession) { + Map keyStatus = drmSession.queryKeyStatus(); + return new Pair<>( + getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING), + getDurationRemainingSec(keyStatus, PROPERTY_PLAYBACK_DURATION_REMAINING)); + } + + private static long getDurationRemainingSec(Map keyStatus, String property) { + if (keyStatus != null) { + try { + String value = keyStatus.get(property); + if (value != null) { + return Long.parseLong(value); + } + } catch (NumberFormatException e) { + // do nothing. + } + } + return C.TIME_UNSET; + } + +} diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java index 72bf23749f..cf27d1ec8b 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java @@ -30,10 +30,10 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; -import com.google.android.exoplayer2.drm.StreamingDrmSessionManager; import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; @@ -701,9 +701,9 @@ public final class DashTest extends ActivityInstrumentationTestCase2 buildDrmSessionManager( + protected final DefaultDrmSessionManager buildDrmSessionManager( final String userAgent) { - StreamingDrmSessionManager drmSessionManager = null; + DefaultDrmSessionManager drmSessionManager = null; if (isWidevineEncrypted) { try { // Force L3 if secure decoder is not available. @@ -717,7 +717,7 @@ public final class DashTest extends ActivityInstrumentationTestCase2