Commit 9d5c750f by eguven Committed by Oliver Woodman

Support offline drm key downloading and restoring

Renamed StreamingDrmSessionManager to DefaultDrmSessionManager and added functionality to download, restore, renew and release offline keys. Added a utility class, OfflineLicenseHelper, to facilitate use of DefaultDrmSessionManager for downloading, renewing and releasing offline keys.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=143769955
parent b2a153d5
...@@ -26,7 +26,7 @@ import com.google.android.exoplayer2.RendererCapabilities; ...@@ -26,7 +26,7 @@ import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters; 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.Metadata;
import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.metadata.id3.ApicFrame;
...@@ -55,7 +55,7 @@ import java.util.Locale; ...@@ -55,7 +55,7 @@ import java.util.Locale;
*/ */
/* package */ final class EventLogger implements ExoPlayer.EventListener, /* package */ final class EventLogger implements ExoPlayer.EventListener,
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener, ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener,
MetadataRenderer.Output { MetadataRenderer.Output {
private static final String TAG = "EventLogger"; private static final String TAG = "EventLogger";
...@@ -279,7 +279,7 @@ import java.util.Locale; ...@@ -279,7 +279,7 @@ import java.util.Locale;
// Do nothing. // Do nothing.
} }
// StreamingDrmSessionManager.EventListener // DefaultDrmSessionManager.EventListener
@Override @Override
public void onDrmSessionManagerError(Exception e) { public void onDrmSessionManagerError(Exception e) {
...@@ -287,6 +287,16 @@ import java.util.Locale; ...@@ -287,6 +287,16 @@ import java.util.Locale;
} }
@Override @Override
public void onDrmKeysRestored() {
Log.d(TAG, "drmKeysRestored [" + getSessionTimeString() + "]");
}
@Override
public void onDrmKeysRemoved() {
Log.d(TAG, "drmKeysRemoved [" + getSessionTimeString() + "]");
}
@Override
public void onDrmKeysLoaded() { public void onDrmKeysLoaded() {
Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]");
} }
......
...@@ -36,11 +36,11 @@ import com.google.android.exoplayer2.ExoPlayer; ...@@ -36,11 +36,11 @@ import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline; 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.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; 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.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
...@@ -358,7 +358,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -358,7 +358,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
} }
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
buildHttpDataSourceFactory(false), keyRequestProperties); buildHttpDataSourceFactory(false), keyRequestProperties);
return new StreamingDrmSessionManager<>(uuid, return new DefaultDrmSessionManager<>(uuid,
FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger); FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger);
} }
......
/*
* 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<ExoMediaCrypto> 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<Long, Long> 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<Long, Long> 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<String, String> 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.<InbandEventStream>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}));
}
}
...@@ -24,7 +24,10 @@ import android.os.Handler; ...@@ -24,7 +24,10 @@ import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.os.Message;
import android.support.annotation.IntDef;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
...@@ -33,18 +36,21 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; ...@@ -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.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map;
import java.util.UUID; 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) @TargetApi(18)
public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T>, public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T>,
DrmSession<T> { DrmSession<T> {
/** /**
* Listener of {@link StreamingDrmSessionManager} events. * Listener of {@link DefaultDrmSessionManager} events.
*/ */
public interface EventListener { public interface EventListener {
...@@ -60,6 +66,16 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm ...@@ -60,6 +66,16 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
*/ */
void onDrmSessionManagerError(Exception e); 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<T extends ExoMediaCrypto> implements Drm ...@@ -67,9 +83,32 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
*/ */
public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData"; 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_PROVISION = 0;
private static final int MSG_KEYS = 1; private static final int MSG_KEYS = 1;
private static final int MAX_LICENSE_DURATION_TO_RENEW = 60;
private final Handler eventHandler; private final Handler eventHandler;
private final EventListener eventListener; private final EventListener eventListener;
private final ExoMediaDrm<T> mediaDrm; private final ExoMediaDrm<T> mediaDrm;
...@@ -85,14 +124,17 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm ...@@ -85,14 +124,17 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
private HandlerThread requestHandlerThread; private HandlerThread requestHandlerThread;
private Handler postRequestHandler; private Handler postRequestHandler;
private int mode;
private int openCount; private int openCount;
private boolean provisioningInProgress; private boolean provisioningInProgress;
@DrmSession.State @DrmSession.State
private int state; private int state;
private T mediaCrypto; private T mediaCrypto;
private Exception lastException; private DrmSessionException lastException;
private SchemeData schemeData; private byte[] schemeInitData;
private String schemeMimeType;
private byte[] sessionId; private byte[] sessionId;
private byte[] offlineLicenseKeySetId;
/** /**
* Instantiates a new instance using the Widevine scheme. * Instantiates a new instance using the Widevine scheme.
...@@ -105,7 +147,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm ...@@ -105,7 +147,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
* @param eventListener A listener of events. 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. * @throws UnsupportedDrmException If the specified DRM scheme is not supported.
*/ */
public static StreamingDrmSessionManager<FrameworkMediaCrypto> newWidevineInstance( public static DefaultDrmSessionManager<FrameworkMediaCrypto> newWidevineInstance(
MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters, MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters,
Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException {
return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters, return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters,
...@@ -125,7 +167,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm ...@@ -125,7 +167,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
* @param eventListener A listener of events. 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. * @throws UnsupportedDrmException If the specified DRM scheme is not supported.
*/ */
public static StreamingDrmSessionManager<FrameworkMediaCrypto> newPlayReadyInstance( public static DefaultDrmSessionManager<FrameworkMediaCrypto> newPlayReadyInstance(
MediaDrmCallback callback, String customData, Handler eventHandler, MediaDrmCallback callback, String customData, Handler eventHandler,
EventListener eventListener) throws UnsupportedDrmException { EventListener eventListener) throws UnsupportedDrmException {
HashMap<String, String> optionalKeyRequestParameters; HashMap<String, String> optionalKeyRequestParameters;
...@@ -151,10 +193,10 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm ...@@ -151,10 +193,10 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
* @param eventListener A listener of events. 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. * @throws UnsupportedDrmException If the specified DRM scheme is not supported.
*/ */
public static StreamingDrmSessionManager<FrameworkMediaCrypto> newFrameworkInstance( public static DefaultDrmSessionManager<FrameworkMediaCrypto> newFrameworkInstance(
UUID uuid, MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters, UUID uuid, MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters,
Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { 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); optionalKeyRequestParameters, eventHandler, eventListener);
} }
...@@ -168,7 +210,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm ...@@ -168,7 +210,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
* null if delivery of events is not required. * null if delivery of events is not required.
* @param eventListener A listener of events. 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.
*/ */
public StreamingDrmSessionManager(UUID uuid, ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback, public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback,
HashMap<String, String> optionalKeyRequestParameters, Handler eventHandler, HashMap<String, String> optionalKeyRequestParameters, Handler eventHandler,
EventListener eventListener) { EventListener eventListener) {
this.uuid = uuid; this.uuid = uuid;
...@@ -179,6 +221,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm ...@@ -179,6 +221,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
this.eventListener = eventListener; this.eventListener = eventListener;
mediaDrm.setOnEventListener(new MediaDrmEventListener()); mediaDrm.setOnEventListener(new MediaDrmEventListener());
state = STATE_CLOSED; state = STATE_CLOSED;
mode = MODE_PLAYBACK;
} }
/** /**
...@@ -229,6 +272,35 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm ...@@ -229,6 +272,35 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
mediaDrm.setPropertyByteArray(key, value); 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.
*
* <p>By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when
* required.
*
* <p>{@code mode} must be one of these:
* <li>{@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is
* requested otherwise the offline license is restored.
* <li>{@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license
* is restored.
* <li>{@link #MODE_DOWNLOAD}: If {@code offlineLicenseKeySetId} is null, an offline license is
* requested otherwise the offline license is renewed.
* <li>{@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. // DrmSessionManager implementation.
@Override @Override
...@@ -248,18 +320,22 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm ...@@ -248,18 +320,22 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
requestHandlerThread.start(); requestHandlerThread.start();
postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
schemeData = drmInitData.get(uuid); if (offlineLicenseKeySetId == null) {
SchemeData schemeData = drmInitData.get(uuid);
if (schemeData == null) { if (schemeData == null) {
onError(new IllegalStateException("Media does not support uuid: " + uuid)); onError(new IllegalStateException("Media does not support uuid: " + uuid));
return this; return this;
} }
schemeInitData = schemeData.data;
schemeMimeType = schemeData.mimeType;
if (Util.SDK_INT < 21) { if (Util.SDK_INT < 21) {
// Prior to L the Widevine CDM required data to be extracted from the PSSH atom. // Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeData.data, C.WIDEVINE_UUID); byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, C.WIDEVINE_UUID);
if (psshData == null) { if (psshData == null) {
// Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
} else { } else {
schemeData = new SchemeData(C.WIDEVINE_UUID, schemeData.mimeType, psshData); schemeInitData = psshData;
}
} }
} }
state = STATE_OPENING; state = STATE_OPENING;
...@@ -280,7 +356,8 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm ...@@ -280,7 +356,8 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
postRequestHandler = null; postRequestHandler = null;
requestHandlerThread.quit(); requestHandlerThread.quit();
requestHandlerThread = null; requestHandlerThread = null;
schemeData = null; schemeInitData = null;
schemeMimeType = null;
mediaCrypto = null; mediaCrypto = null;
lastException = null; lastException = null;
if (sessionId != null) { if (sessionId != null) {
...@@ -314,10 +391,25 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm ...@@ -314,10 +391,25 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
} }
@Override @Override
public final Exception getError() { public final DrmSessionException getError() {
return state == STATE_ERROR ? lastException : null; return state == STATE_ERROR ? lastException : null;
} }
@Override
public Map<String, String> 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. // Internal methods.
private void openInternal(boolean allowProvisioning) { private void openInternal(boolean allowProvisioning) {
...@@ -325,7 +417,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm ...@@ -325,7 +417,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
sessionId = mediaDrm.openSession(); sessionId = mediaDrm.openSession();
mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId); mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId);
state = STATE_OPENED; state = STATE_OPENED;
postKeyRequest(); doLicense();
} catch (NotProvisionedException e) { } catch (NotProvisionedException e) {
if (allowProvisioning) { if (allowProvisioning) {
postProvisionRequest(); postProvisionRequest();
...@@ -363,20 +455,87 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm ...@@ -363,20 +455,87 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
if (state == STATE_OPENING) { if (state == STATE_OPENING) {
openInternal(false); openInternal(false);
} else { } else {
postKeyRequest(); doLicense();
} }
} catch (DeniedByServerException e) { } catch (DeniedByServerException e) {
onError(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<Long, Long> pair = WidevineUtil.getLicenseDurationRemainingSec(this);
return Math.min(pair.first, pair.second);
}
private void postKeyRequest(byte[] scope, int keyType) {
KeyRequest keyRequest; KeyRequest keyRequest;
try { try {
keyRequest = mediaDrm.getKeyRequest(sessionId, schemeData.data, schemeData.mimeType, keyRequest = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, keyType,
MediaDrm.KEY_TYPE_STREAMING, optionalKeyRequestParameters); optionalKeyRequestParameters);
postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget(); postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
} catch (NotProvisionedException e) { } catch (Exception e) {
onKeysError(e); onKeysError(e);
} }
} }
...@@ -393,7 +552,23 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm ...@@ -393,7 +552,23 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
} }
try { try {
mediaDrm.provideKeyResponse(sessionId, (byte[]) response); 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; state = STATE_OPENED_WITH_KEYS;
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
...@@ -403,6 +578,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm ...@@ -403,6 +578,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
} }
}); });
} }
}
} catch (Exception e) { } catch (Exception e) {
onKeysError(e); onKeysError(e);
} }
...@@ -417,7 +593,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm ...@@ -417,7 +593,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
} }
private void onError(final Exception e) { private void onError(final Exception e) {
lastException = e; lastException = new DrmSessionException(e);
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
@Override @Override
...@@ -446,11 +622,16 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm ...@@ -446,11 +622,16 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
} }
switch (msg.what) { switch (msg.what) {
case MediaDrm.EVENT_KEY_REQUIRED: case MediaDrm.EVENT_KEY_REQUIRED:
postKeyRequest(); doLicense();
break; break;
case MediaDrm.EVENT_KEY_EXPIRED: case MediaDrm.EVENT_KEY_EXPIRED:
// 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; state = STATE_OPENED;
onError(new KeysExpiredException()); onError(new KeysExpiredException());
}
break; break;
case MediaDrm.EVENT_PROVISION_REQUIRED: case MediaDrm.EVENT_PROVISION_REQUIRED:
state = STATE_OPENED; state = STATE_OPENED;
...@@ -466,8 +647,10 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm ...@@ -466,8 +647,10 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
@Override @Override
public void onEvent(ExoMediaDrm<? extends T> md, byte[] sessionId, int event, int extra, public void onEvent(ExoMediaDrm<? extends T> md, byte[] sessionId, int event, int extra,
byte[] data) { byte[] data) {
if (mode == MODE_PLAYBACK) {
mediaDrmHandler.sendEmptyMessage(event); mediaDrmHandler.sendEmptyMessage(event);
} }
}
} }
......
...@@ -16,9 +16,11 @@ ...@@ -16,9 +16,11 @@
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.media.MediaDrm;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.Map;
/** /**
* A DRM session. * A DRM session.
...@@ -26,6 +28,15 @@ import java.lang.annotation.RetentionPolicy; ...@@ -26,6 +28,15 @@ import java.lang.annotation.RetentionPolicy;
@TargetApi(16) @TargetApi(16)
public interface DrmSession<T extends ExoMediaCrypto> { public interface DrmSession<T extends ExoMediaCrypto> {
/** 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. * The state of the DRM session.
*/ */
...@@ -96,6 +107,26 @@ public interface DrmSession<T extends ExoMediaCrypto> { ...@@ -96,6 +107,26 @@ public interface DrmSession<T extends ExoMediaCrypto> {
* *
* @return An exception if the state is {@link #STATE_ERROR}. Null otherwise. * @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.
*
* <p>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<String, String> queryKeyStatus();
/**
* Returns the key set id of the offline license loaded into this session, if there is one. Null
* otherwise.
*/
byte[] getOfflineLicenseKeySetId();
} }
/*
* 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<T extends ExoMediaCrypto> {
private final ConditionVariable conditionVariable;
private final DefaultDrmSessionManager<T> 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<FrameworkMediaCrypto> 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<FrameworkMediaCrypto> newWidevineInstance(
MediaDrmCallback callback, HashMap<String, String> 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<T> mediaDrm, MediaDrmCallback callback,
HashMap<String, String> 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<Long, Long> getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId)
throws DrmSessionException {
Assertions.checkNotNull(offlineLicenseKeySetId);
DrmSession<T> session = openBlockingKeyRequest(DefaultDrmSessionManager.MODE_QUERY,
offlineLicenseKeySetId, null);
Pair<Long, Long> licenseDurationRemainingSec =
WidevineUtil.getLicenseDurationRemainingSec(drmSessionManager);
drmSessionManager.releaseSession(session);
return licenseDurationRemainingSec;
}
private void blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId,
DrmInitData drmInitData) throws DrmSessionException {
DrmSession<T> session = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId,
drmInitData);
DrmSessionException error = session.getError();
if (error != null) {
throw error;
}
drmSessionManager.releaseSession(session);
}
private DrmSession<T> openBlockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId,
DrmInitData drmInitData) {
drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId);
conditionVariable.close();
DrmSession<T> 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 */);
}
}
/*
* 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<Long, Long> getLicenseDurationRemainingSec(DrmSession drmSession) {
Map<String, String> keyStatus = drmSession.queryKeyStatus();
return new Pair<>(
getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING),
getDurationRemainingSec(keyStatus, PROPERTY_PLAYBACK_DURATION_REMAINING));
}
private static long getDurationRemainingSec(Map<String, String> 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;
}
}
...@@ -30,10 +30,10 @@ import com.google.android.exoplayer2.Format; ...@@ -30,10 +30,10 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.decoder.DecoderCounters; 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.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; 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.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
...@@ -701,9 +701,9 @@ public final class DashTest extends ActivityInstrumentationTestCase2<HostActivit ...@@ -701,9 +701,9 @@ public final class DashTest extends ActivityInstrumentationTestCase2<HostActivit
@Override @Override
@TargetApi(18) @TargetApi(18)
@SuppressWarnings("ResourceType") @SuppressWarnings("ResourceType")
protected final StreamingDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManager( protected final DefaultDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManager(
final String userAgent) { final String userAgent) {
StreamingDrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null; DefaultDrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
if (isWidevineEncrypted) { if (isWidevineEncrypted) {
try { try {
// Force L3 if secure decoder is not available. // Force L3 if secure decoder is not available.
...@@ -717,7 +717,7 @@ public final class DashTest extends ActivityInstrumentationTestCase2<HostActivit ...@@ -717,7 +717,7 @@ public final class DashTest extends ActivityInstrumentationTestCase2<HostActivit
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback( HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(
WIDEVINE_LICENSE_URL + widevineContentId, WIDEVINE_LICENSE_URL + widevineContentId,
new DefaultHttpDataSourceFactory(userAgent)); new DefaultHttpDataSourceFactory(userAgent));
drmSessionManager = StreamingDrmSessionManager.newWidevineInstance(drmCallback, null, drmSessionManager = DefaultDrmSessionManager.newWidevineInstance(drmCallback, null,
null, null); null, null);
if (forceL3Widevine && !WIDEVINE_SECURITY_LEVEL_3.equals(securityProperty)) { if (forceL3Widevine && !WIDEVINE_SECURITY_LEVEL_3.equals(securityProperty)) {
drmSessionManager.setPropertyString(SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3); drmSessionManager.setPropertyString(SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment