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}));
}
}
...@@ -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) 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