DrmSession cleanup

These changes are in part related to handling playback of mixed clear
and encrypted content, where we might want to use a secure decoder
throughout, but only have drm init data and only care about the state
of the DrmSession during playback of encrypted parts.

- requiresSecureDecoderComponent became unnecessary when we added
  ExoMediaCrypto, which provides a layer in which requiresSecureDecoderComponent
  can be overridden.
- Relaxed requirements for obtaining the MediaCrypto. It's helpful
  to allow retrieval in the error state, since it can be used to
  instantiate a decoder and play clear samples.
- Deferred throwing of errors in renderer implementations. As long as
  we can get a MediaCrypto, we should init the codec. We can also
  play clear samples without failing if playClearSamplesWithoutKeys is
  true, regardless of the errors state.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=160536365
This commit is contained in:
olly 2017-06-29 09:41:36 -07:00 committed by Oliver Woodman
parent beb0734107
commit db177db6d9
9 changed files with 124 additions and 142 deletions

View file

@ -30,6 +30,7 @@ import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.Assertions;
@ -185,42 +186,21 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
}
// We have a format.
drmSession = pendingDrmSession;
ExoMediaCrypto mediaCrypto = null;
if (drmSession != null) {
int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
} else if (drmSessionState == DrmSession.STATE_OPENED
|| drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
mediaCrypto = drmSession.getMediaCrypto();
} else {
// The drm session isn't open yet.
return;
}
}
try {
if (decoder == null) {
// If we don't have a decoder yet, we need to instantiate one.
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
TraceUtil.beginSection("createVpxDecoder");
decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, mediaCrypto);
decoder.setOutputMode(outputMode);
// If we don't have a decoder yet, we need to instantiate one.
maybeInitDecoder();
if (decoder != null) {
try {
// Rendering loop.
TraceUtil.beginSection("drainAndFeed");
while (drainOutputBuffer(positionUs)) {}
while (feedInputBuffer()) {}
TraceUtil.endSection();
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
codecInitializedTimestamp - codecInitializingTimestamp);
decoderCounters.decoderInitCount++;
} catch (VpxDecoderException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
TraceUtil.beginSection("drainAndFeed");
while (drainOutputBuffer(positionUs)) {}
while (feedInputBuffer()) {}
TraceUtil.endSection();
} catch (VpxDecoderException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
decoderCounters.ensureUpdated();
}
decoderCounters.ensureUpdated();
}
private boolean drainOutputBuffer(long positionUs) throws VpxDecoderException {
@ -399,15 +379,14 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
if (drmSession == null) {
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false;
}
int drmSessionState = drmSession.getState();
@DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
}
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
&& (bufferEncrypted || !playClearSamplesWithoutKeys);
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
}
private void flushDecoder() {
@ -516,6 +495,40 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
}
private void maybeInitDecoder() throws ExoPlaybackException {
if (decoder != null) {
return;
}
drmSession = pendingDrmSession;
ExoMediaCrypto mediaCrypto = null;
if (drmSession != null) {
mediaCrypto = drmSession.getMediaCrypto();
if (mediaCrypto == null) {
DrmSessionException drmError = drmSession.getError();
if (drmError != null) {
throw ExoPlaybackException.createForRenderer(drmError, getIndex());
}
// The drm session isn't open yet.
return;
}
}
try {
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
TraceUtil.beginSection("createVpxDecoder");
decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, mediaCrypto);
decoder.setOutputMode(outputMode);
TraceUtil.endSection();
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
codecInitializedTimestamp - codecInitializingTimestamp);
decoderCounters.decoderInitCount++;
} catch (VpxDecoderException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
}
private void releaseDecoder() {
if (decoder != null) {
decoder.release();

View file

@ -1463,7 +1463,7 @@ public final class AudioTrack {
@TargetApi(21)
private int writeNonBlockingWithAvSyncV21(android.media.AudioTrack audioTrack,
ByteBuffer buffer, int size, long presentationTimeUs) {
// TODO: Uncomment this when [Internal ref b/33627517] is clarified or fixed.
// TODO: Uncomment this when [Internal ref: b/33627517] is clarified or fixed.
// if (Util.SDK_INT >= 23) {
// // The underlying platform AudioTrack writes AV sync headers directly.
// return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000);

View file

@ -32,6 +32,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.Assertions;
@ -376,15 +377,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
if (drmSession == null) {
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false;
}
@DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
}
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
&& (bufferEncrypted || !playClearSamplesWithoutKeys);
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
}
private void processEndOfStream() throws ExoPlaybackException {
@ -514,13 +514,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
drmSession = pendingDrmSession;
ExoMediaCrypto mediaCrypto = null;
if (drmSession != null) {
@DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
} else if (drmSessionState == DrmSession.STATE_OPENED
|| drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
mediaCrypto = drmSession.getMediaCrypto();
} else {
mediaCrypto = drmSession.getMediaCrypto();
if (mediaCrypto == null) {
DrmSessionException drmError = drmSession.getError();
if (drmError != null) {
throw ExoPlaybackException.createForRenderer(drmError, getIndex());
}
// The drm session isn't open yet.
return;
}

View file

@ -222,7 +222,6 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
this.eventHandler = eventHandler;
this.eventListener = eventListener;
mediaDrm.setOnEventListener(new MediaDrmEventListener());
state = STATE_CLOSED;
mode = MODE_PLAYBACK;
}
@ -358,7 +357,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
if (--openCount != 0) {
return;
}
state = STATE_CLOSED;
state = STATE_RELEASED;
provisioningInProgress = false;
mediaDrmHandler.removeCallbacksAndMessages(null);
postResponseHandler.removeCallbacksAndMessages(null);
@ -384,35 +383,19 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
return state;
}
@Override
public final T getMediaCrypto() {
if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
throw new IllegalStateException();
}
return mediaCrypto;
}
@Override
public boolean requiresSecureDecoderComponent(String mimeType) {
if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
throw new IllegalStateException();
}
return mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
@Override
public final DrmSessionException getError() {
return state == STATE_ERROR ? lastException : null;
}
@Override
public final T getMediaCrypto() {
return mediaCrypto;
}
@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);
return sessionId == null ? null : mediaDrm.queryKeyStatus(sessionId);
}
@Override

View file

@ -41,16 +41,16 @@ public interface DrmSession<T extends ExoMediaCrypto> {
* The state of the DRM session.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_ERROR, STATE_CLOSED, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS})
@IntDef({STATE_RELEASED, STATE_ERROR, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS})
@interface State {}
/**
* The session has been released.
*/
int STATE_RELEASED = 0;
/**
* The session has encountered an error. {@link #getError()} can be used to retrieve the cause.
*/
int STATE_ERROR = 0;
/**
* The session is closed.
*/
int STATE_CLOSED = 1;
int STATE_ERROR = 1;
/**
* The session is being opened.
*/
@ -65,66 +65,40 @@ public interface DrmSession<T extends ExoMediaCrypto> {
int STATE_OPENED_WITH_KEYS = 4;
/**
* Returns the current state of the session.
*
* @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING},
* {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}.
* Returns the current state of the session, which is one of {@link #STATE_ERROR},
* {@link #STATE_RELEASED}, {@link #STATE_OPENING}, {@link #STATE_OPENED} and
* {@link #STATE_OPENED_WITH_KEYS}.
*/
@State int getState();
/**
* Returns a {@link ExoMediaCrypto} for the open session.
* <p>
* This method may be called when the session is in the following states:
* {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS}
*
* @return A {@link ExoMediaCrypto} for the open session.
* @throws IllegalStateException If called when a session isn't opened.
*/
T getMediaCrypto();
/**
* Whether the session requires a secure decoder for the specified mime type.
* <p>
* Normally this method should return
* {@link ExoMediaCrypto#requiresSecureDecoderComponent(String)}, however in some cases
* implementations may wish to modify the return value (i.e. to force a secure decoder even when
* one is not required).
* <p>
* This method may be called when the session is in the following states:
* {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS}
*
* @return Whether the open session requires a secure decoder for the specified mime type.
* @throws IllegalStateException If called when a session isn't opened.
*/
boolean requiresSecureDecoderComponent(String mimeType);
/**
* Returns the cause of the error state.
* <p>
* This method may be called when the session is in any state.
*
* @return An exception if the state is {@link #STATE_ERROR}. Null otherwise.
*/
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
* Returns a {@link ExoMediaCrypto} for the open session, or null if called before the session has
* been opened or after it's been released.
*/
T getMediaCrypto();
/**
* Returns a map describing the key status for the session, or null if called before the session
* has been opened or after it's been released.
* <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.
* @return A map describing the key status for the session, or null if called before the session
* has been opened or after it's been released.
* @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.
* Returns the key set id of the offline license loaded into this session, or null if there isn't
* one.
*/
byte[] getOfflineLicenseKeySetId();

View file

@ -26,9 +26,12 @@ import com.google.android.exoplayer2.util.Assertions;
public final class FrameworkMediaCrypto implements ExoMediaCrypto {
private final MediaCrypto mediaCrypto;
private final boolean forceAllowInsecureDecoderComponents;
/* package */ FrameworkMediaCrypto(MediaCrypto mediaCrypto) {
/* package */ FrameworkMediaCrypto(MediaCrypto mediaCrypto,
boolean forceAllowInsecureDecoderComponents) {
this.mediaCrypto = Assertions.checkNotNull(mediaCrypto);
this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents;
}
public MediaCrypto getWrappedMediaCrypto() {
@ -37,7 +40,8 @@ public final class FrameworkMediaCrypto implements ExoMediaCrypto {
@Override
public boolean requiresSecureDecoderComponent(String mimeType) {
return mediaCrypto.requiresSecureDecoderComponent(mimeType);
return !forceAllowInsecureDecoderComponents
&& mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
}

View file

@ -24,7 +24,9 @@ import android.media.NotProvisionedException;
import android.media.ResourceBusyException;
import android.media.UnsupportedSchemeException;
import android.support.annotation.NonNull;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@ -163,7 +165,12 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
@Override
public FrameworkMediaCrypto createMediaCrypto(UUID uuid, byte[] initData)
throws MediaCryptoException {
return new FrameworkMediaCrypto(new MediaCrypto(uuid, initData));
// Work around a bug prior to Lollipop where L1 Widevine forced into L3 mode would still
// indicate that it required secure video decoders [Internal ref: b/11428937].
boolean forceAllowInsecureDecoderComponents = Util.SDK_INT < 21
&& C.WIDEVINE_UUID.equals(uuid) && "L3".equals(getPropertyString("securityLevel"));
return new FrameworkMediaCrypto(new MediaCrypto(uuid, initData),
forceAllowInsecureDecoderComponents);
}
}

View file

@ -34,14 +34,16 @@ public final class 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
* @param drmSession The drm session to query.
* @return A {@link Pair} consisting of the remaining license and playback durations in seconds,
* or null if called before the session has been opened or after it's been released.
*/
public static Pair<Long, Long> getLicenseDurationRemainingSec(DrmSession<?> drmSession) {
Map<String, String> keyStatus = drmSession.queryKeyStatus();
return new Pair<>(
getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING),
if (keyStatus == null) {
return null;
}
return new Pair<>(getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING),
getDurationRemainingSec(keyStatus, PROPERTY_PLAYBACK_DURATION_REMAINING));
}

View file

@ -32,6 +32,7 @@ import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
@ -298,20 +299,20 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
drmSession = pendingDrmSession;
String mimeType = format.sampleMimeType;
MediaCrypto mediaCrypto = null;
MediaCrypto wrappedMediaCrypto = null;
boolean drmSessionRequiresSecureDecoder = false;
if (drmSession != null) {
@DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
} else if (drmSessionState == DrmSession.STATE_OPENED
|| drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
mediaCrypto = drmSession.getMediaCrypto().getWrappedMediaCrypto();
drmSessionRequiresSecureDecoder = drmSession.requiresSecureDecoderComponent(mimeType);
} else {
FrameworkMediaCrypto mediaCrypto = drmSession.getMediaCrypto();
if (mediaCrypto == null) {
DrmSessionException drmError = drmSession.getError();
if (drmError != null) {
throw ExoPlaybackException.createForRenderer(drmError, getIndex());
}
// The drm session isn't open yet.
return;
}
wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto();
drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
if (codecInfo == null) {
@ -358,7 +359,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codec = MediaCodec.createByCodecName(codecName);
TraceUtil.endSection();
TraceUtil.beginSection("configureCodec");
configureCodec(codecInfo, codec, format, mediaCrypto);
configureCodec(codecInfo, codec, format, wrappedMediaCrypto);
TraceUtil.endSection();
TraceUtil.beginSection("startCodec");
codec.start();
@ -736,15 +737,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
if (drmSession == null) {
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false;
}
@DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
}
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
&& (bufferEncrypted || !playClearSamplesWithoutKeys);
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
}
/**