mirror of
https://github.com/samsonjs/media.git
synced 2026-06-29 05:39:31 +00:00
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
This commit is contained in:
parent
b2a153d568
commit
9d5c750fe9
8 changed files with 883 additions and 54 deletions
|
|
@ -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() + "]");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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;
|
|||
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<T extends ExoMediaCrypto> implements DrmSessionManager<T>,
|
||||
public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T>,
|
||||
DrmSession<T> {
|
||||
|
||||
/**
|
||||
* Listener of {@link StreamingDrmSessionManager} events.
|
||||
* Listener of {@link DefaultDrmSessionManager} events.
|
||||
*/
|
||||
public interface EventListener {
|
||||
|
||||
|
|
@ -60,6 +66,16 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> 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<T extends ExoMediaCrypto> 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<T> mediaDrm;
|
||||
|
|
@ -85,14 +124,17 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> 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<T extends ExoMediaCrypto> 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<FrameworkMediaCrypto> newWidevineInstance(
|
||||
public static DefaultDrmSessionManager<FrameworkMediaCrypto> newWidevineInstance(
|
||||
MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters,
|
||||
Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException {
|
||||
return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters,
|
||||
|
|
@ -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.
|
||||
* @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,
|
||||
EventListener eventListener) throws UnsupportedDrmException {
|
||||
HashMap<String, String> optionalKeyRequestParameters;
|
||||
|
|
@ -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.
|
||||
* @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,
|
||||
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<T extends ExoMediaCrypto> 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<T> mediaDrm, MediaDrmCallback callback,
|
||||
public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback,
|
||||
HashMap<String, String> optionalKeyRequestParameters, Handler eventHandler,
|
||||
EventListener eventListener) {
|
||||
this.uuid = uuid;
|
||||
|
|
@ -179,6 +221,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
this.eventListener = eventListener;
|
||||
mediaDrm.setOnEventListener(new MediaDrmEventListener());
|
||||
state = STATE_CLOSED;
|
||||
mode = MODE_PLAYBACK;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -229,6 +272,35 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> 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.
|
||||
*
|
||||
* <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.
|
||||
|
||||
@Override
|
||||
|
|
@ -248,18 +320,22 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> 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<T extends ExoMediaCrypto> 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<T extends ExoMediaCrypto> implements Drm
|
|||
}
|
||||
|
||||
@Override
|
||||
public final Exception getError() {
|
||||
public final DrmSessionException getError() {
|
||||
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.
|
||||
|
||||
private void openInternal(boolean allowProvisioning) {
|
||||
|
|
@ -325,7 +417,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> 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<T extends ExoMediaCrypto> 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<Long, Long> 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<T extends ExoMediaCrypto> 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<T extends ExoMediaCrypto> 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<T extends ExoMediaCrypto> 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<T extends ExoMediaCrypto> implements Drm
|
|||
@Override
|
||||
public void onEvent(ExoMediaDrm<? extends T> md, byte[] sessionId, int event, int extra,
|
||||
byte[] data) {
|
||||
mediaDrmHandler.sendEmptyMessage(event);
|
||||
if (mode == MODE_PLAYBACK) {
|
||||
mediaDrmHandler.sendEmptyMessage(event);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<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.
|
||||
*/
|
||||
|
|
@ -96,6 +107,26 @@ public interface DrmSession<T extends ExoMediaCrypto> {
|
|||
*
|
||||
* @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();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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 */);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<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;
|
|||
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<HostActivit
|
|||
@Override
|
||||
@TargetApi(18)
|
||||
@SuppressWarnings("ResourceType")
|
||||
protected final StreamingDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManager(
|
||||
protected final DefaultDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManager(
|
||||
final String userAgent) {
|
||||
StreamingDrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
|
||||
DefaultDrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null;
|
||||
if (isWidevineEncrypted) {
|
||||
try {
|
||||
// Force L3 if secure decoder is not available.
|
||||
|
|
@ -717,7 +717,7 @@ public final class DashTest extends ActivityInstrumentationTestCase2<HostActivit
|
|||
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(
|
||||
WIDEVINE_LICENSE_URL + widevineContentId,
|
||||
new DefaultHttpDataSourceFactory(userAgent));
|
||||
drmSessionManager = StreamingDrmSessionManager.newWidevineInstance(drmCallback, null,
|
||||
drmSessionManager = DefaultDrmSessionManager.newWidevineInstance(drmCallback, null,
|
||||
null, null);
|
||||
if (forceL3Widevine && !WIDEVINE_SECURITY_LEVEL_3.equals(securityProperty)) {
|
||||
drmSessionManager.setPropertyString(SECURITY_LEVEL_PROPERTY, WIDEVINE_SECURITY_LEVEL_3);
|
||||
|
|
|
|||
Loading…
Reference in a new issue