Commit db177db6 by olly Committed by Oliver Woodman

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
parent beb07341
......@@ -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();
......
......@@ -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);
......
......@@ -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;
}
......
......@@ -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);
......@@ -385,34 +384,18 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
}
@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);
public final DrmSessionException getError() {
return state == STATE_ERROR ? lastException : null;
}
@Override
public final DrmSessionException getError() {
return state == STATE_ERROR ? lastException : null;
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
......
......@@ -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 encountered an error. {@link #getError()} can be used to retrieve the cause.
* The session has been released.
*/
int STATE_ERROR = 0;
int STATE_RELEASED = 0;
/**
* The session is closed.
* The session has encountered an error. {@link #getError()} can be used to retrieve the cause.
*/
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.
* Returns the cause of the error state.
*/
T getMediaCrypto();
DrmSessionException getError();
/**
* 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.
* Returns a {@link ExoMediaCrypto} for the open session, or null if called before the session has
* been opened or after it's been released.
*/
boolean requiresSecureDecoderComponent(String mimeType);
T getMediaCrypto();
/**
* Returns the cause of the error state.
* 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>
* 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
* 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();
......
......@@ -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);
}
}
......@@ -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);
}
}
......@@ -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));
}
......
......@@ -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;
}
/**
......
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