Clean up logic for determining whether DRM reconfig needs codec re-init

1. Move logic to decide to re-initialize the codec rather than using
   MediaCodec.setMediaDrmSession if (a) PlayReady is in use, and (b)
   the new session is still provisioning. This would previously have
   happened asynchronously after an input format change, after the
   decoder has subsequently been flushed. After this change the logic
   executes synchronously when the input format changes. This helps
   with the ref'd bug, since we want to propagate reasons for codec
   re-initialization through inputFormatChanged events.
2. Whilst moving the logic for re-initialization if PlayReady is
   being used, I fixed a bug that would occur when switching from
   [PlayReady --> non-PlayReady]. Re-use doesn't work in this case.
   The old logic only checked for the [Something --> PlayReady] case.
3. Remove pointless codec flush if updating the DRM session having
   not queued anything to the codec.

PiperOrigin-RevId: 340299790
This commit is contained in:
olly 2020-11-02 21:09:21 +00:00 committed by Oliver Woodman
parent 9d3875a860
commit 2c7473dc05
5 changed files with 103 additions and 67 deletions

View File

@ -34,6 +34,9 @@
* Matroska: Add support for 32-bit floating point PCM, and 8-bit and
16-bit big endian integer PCM
([#8142](https://github.com/google/ExoPlayer/issues/8142)).
* DRM:
* Fix playback failure when switching from PlayReady protected content to
Widevine or Clearkey protected content in a playlist.
* IMA extension:
* Upgrade IMA SDK dependency to 3.21.0, and release the `AdsLoader`
([#7344](https://github.com/google/ExoPlayer/issues/7344)).

View File

@ -256,6 +256,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
return state == STATE_ERROR ? lastException : null;
}
@Override
public final UUID getSchemeUuid() {
return uuid;
}
@Override
public final @Nullable ExoMediaCrypto getMediaCrypto() {
return mediaCrypto;

View File

@ -23,6 +23,7 @@ import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Map;
import java.util.UUID;
/** A DRM session. */
public interface DrmSession {
@ -101,6 +102,9 @@ public interface DrmSession {
@Nullable
DrmSessionException getError();
/** Returns the DRM scheme UUID for this session. */
UUID getSchemeUuid();
/**
* Returns an {@link ExoMediaCrypto} for the open session, or null if called before the session
* has been opened or after it's been released.

View File

@ -16,8 +16,10 @@
package com.google.android.exoplayer2.drm;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import java.util.Map;
import java.util.UUID;
/** A {@link DrmSession} that's in a terminal error state. */
public final class ErrorStateDrmSession implements DrmSession {
@ -44,6 +46,11 @@ public final class ErrorStateDrmSession implements DrmSession {
return error;
}
@Override
public final UUID getSchemeUuid() {
return C.UUID_NIL;
}
@Override
@Nullable
public ExoMediaCrypto getMediaCrypto() {

View File

@ -19,6 +19,7 @@ import static com.google.android.exoplayer2.mediacodec.MediaCodecInfo.KEEP_CODEC
import static com.google.android.exoplayer2.mediacodec.MediaCodecInfo.KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION;
import static com.google.android.exoplayer2.mediacodec.MediaCodecInfo.KEEP_CODEC_RESULT_YES_WITH_FLUSH;
import static com.google.android.exoplayer2.mediacodec.MediaCodecInfo.KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static java.lang.Math.max;
@ -373,7 +374,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
boolean enableDecoderFallback,
float assumedMinimumCodecOperatingRate) {
super(trackType);
this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector);
this.mediaCodecSelector = checkNotNull(mediaCodecSelector);
this.enableDecoderFallback = enableDecoderFallback;
this.assumedMinimumCodecOperatingRate = assumedMinimumCodecOperatingRate;
buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
@ -1385,7 +1386,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
@CallSuper
protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException {
waitingForFirstSampleInFormat = true;
Format newFormat = Assertions.checkNotNull(formatHolder.format);
Format newFormat = checkNotNull(formatHolder.format);
setSourceDrmSession(formatHolder.drmSession);
inputFormat = newFormat;
@ -1402,22 +1403,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return;
}
// We have an existing codec that we may need to reconfigure or re-initialize or release it to
// switch to bypass. If the existing codec instance is being kept then its operating rate
// may need to be updated.
// We have an existing codec that we may need to reconfigure, re-initialize, or release to
// switch to bypass. If the existing codec instance is kept then its operating rate and DRM
// session may need to be updated.
if ((sourceDrmSession == null && codecDrmSession != null)
|| (sourceDrmSession != null && codecDrmSession == null)
|| (sourceDrmSession != codecDrmSession
&& !codecInfo.secure
&& maybeRequiresSecureDecoder(sourceDrmSession, newFormat))
|| (Util.SDK_INT < 23 && sourceDrmSession != codecDrmSession)) {
// We might need to switch between the clear and protected output paths, or we're using DRM
// prior to API level 23 where the codec needs to be re-initialized to switch to the new DRM
// session.
if (drmNeedsCodecReinitialization(codecInfo, newFormat, codecDrmSession, sourceDrmSession)) {
drainAndReinitializeCodec();
return;
}
boolean drainAndUpdateCodecDrmSession = sourceDrmSession != codecDrmSession;
Assertions.checkState(!drainAndUpdateCodecDrmSession || Util.SDK_INT >= 23);
switch (canKeepCodec(codec, codecInfo, codecInputFormat, newFormat)) {
case KEEP_CODEC_RESULT_NO:
@ -1426,8 +1421,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
case KEEP_CODEC_RESULT_YES_WITH_FLUSH:
codecInputFormat = newFormat;
updateCodecOperatingRate();
if (sourceDrmSession != codecDrmSession) {
drainAndUpdateCodecDrmSession();
if (drainAndUpdateCodecDrmSession) {
drainAndUpdateCodecDrmSessionV23();
} else {
drainAndFlushCodec();
}
@ -1445,16 +1440,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
&& newFormat.height == codecInputFormat.height);
codecInputFormat = newFormat;
updateCodecOperatingRate();
if (sourceDrmSession != codecDrmSession) {
drainAndUpdateCodecDrmSession();
if (drainAndUpdateCodecDrmSession) {
drainAndUpdateCodecDrmSessionV23();
}
}
break;
case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION:
codecInputFormat = newFormat;
updateCodecOperatingRate();
if (sourceDrmSession != codecDrmSession) {
drainAndUpdateCodecDrmSession();
if (drainAndUpdateCodecDrmSession) {
drainAndUpdateCodecDrmSessionV23();
}
break;
default:
@ -1652,18 +1647,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*
* @throws ExoPlaybackException If an error occurs updating the codec's DRM session.
*/
private void drainAndUpdateCodecDrmSession() throws ExoPlaybackException {
if (Util.SDK_INT < 23) {
// The codec needs to be re-initialized to switch to the source DRM session.
drainAndReinitializeCodec();
return;
}
@TargetApi(23) // Only called when SDK_INT >= 23, but lint isn't clever enough to know.
private void drainAndUpdateCodecDrmSessionV23() throws ExoPlaybackException {
if (codecReceivedBuffers) {
codecDrainState = DRAIN_STATE_SIGNAL_END_OF_STREAM;
codecDrainAction = DRAIN_ACTION_UPDATE_DRM_SESSION;
} else {
// Nothing has been queued to the decoder, so we can do the update immediately.
updateDrmSessionOrReinitializeCodecV23();
updateDrmSessionV23();
}
}
@ -1902,7 +1893,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
reinitializeCodec();
break;
case DRAIN_ACTION_UPDATE_DRM_SESSION:
updateDrmSessionOrReinitializeCodecV23();
if (!flushOrReinitializeCodec()) {
updateDrmSessionV23();
}
break;
case DRAIN_ACTION_FLUSH:
flushOrReinitializeCodec();
@ -1950,24 +1943,72 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|| FrameworkMediaCrypto.class.equals(format.exoMediaCryptoType);
}
/**
* Returns whether it's necessary to re-initialize the codec to handle a DRM change. If {@code
* false} is returned then either {@code oldSession == newSession} (i.e., there was no change), or
* it's possible to update the existing codec using MediaCrypto.setMediaDrmSession.
*/
private boolean drmNeedsCodecReinitialization(
MediaCodecInfo codecInfo,
Format newFormat,
@Nullable DrmSession oldSession,
@Nullable DrmSession newSession)
throws ExoPlaybackException {
if (oldSession == newSession) {
// No need to re-initialize if the old and new sessions are the same.
return false;
}
// Note: At least one of oldSession and newSession are non-null.
if (newSession == null || oldSession == null) {
// Changing from DRM to no DRM and vice-versa always requires re-initialization.
return true;
}
// Note: Both oldSession and newSession are non-null, and they are different sessions.
if (Util.SDK_INT < 23) {
// MediaCrypto.setMediaDrmSession is only available from API level 23, so re-initialization is
// required to switch to newSession on older API levels.
return true;
}
if (C.PLAYREADY_UUID.equals(oldSession.getSchemeUuid())
|| C.PLAYREADY_UUID.equals(newSession.getSchemeUuid())) {
// The PlayReady CDM does not support MediaCrypto.setMediaDrmSession, either as the old or new
// session.
// TODO: Add an API check once [Internal ref: b/128835874] is fixed.
return true;
}
@Nullable FrameworkMediaCrypto newMediaCrypto = getFrameworkMediaCrypto(newSession);
if (newMediaCrypto == null) {
// We'd only expect this to happen if the CDM from which newSession is obtained needs
// provisioning. This is unlikely to happen (it probably requires a switch from one DRM scheme
// to another, where the new CDM hasn't been used before and needs provisioning). It would be
// possible to handle this case without codec re-initialization, but it would require the
// re-use code path to be able to wait for provisioning to finish before calling
// MediaCrypto.setMediaDrmSession. The extra complexity is not warranted given how unlikely
// the case is to occur, so we re-initialize in this case.
return true;
}
if (!codecInfo.secure && maybeRequiresSecureDecoder(newMediaCrypto, newFormat)) {
// Re-initialization is required because newSession might require switching to the secure
// output path.
return true;
}
return false;
}
/**
* Returns whether a {@link DrmSession} may require a secure decoder for a given {@link Format}.
*
* @param drmSession The {@link DrmSession}.
* @param sessionMediaCrypto The {@link DrmSession}'s {@link FrameworkMediaCrypto}.
* @param format The {@link Format}.
* @return Whether a secure decoder may be required.
*/
private boolean maybeRequiresSecureDecoder(DrmSession drmSession, Format format)
throws ExoPlaybackException {
// MediaCrypto type is checked during track selection.
@Nullable FrameworkMediaCrypto sessionMediaCrypto = getFrameworkMediaCrypto(drmSession);
if (sessionMediaCrypto == null) {
// We'd only expect this to happen if the CDM from which the pending session is obtained needs
// provisioning. This is unlikely to happen (it probably requires a switch from one DRM scheme
// to another, where the new CDM hasn't been used before and needs provisioning). Assume that
// a secure decoder may be required.
return true;
}
private boolean maybeRequiresSecureDecoder(
FrameworkMediaCrypto sessionMediaCrypto, Format format) {
if (sessionMediaCrypto.forceAllowInsecureDecoderComponents) {
return false;
}
@ -2004,33 +2045,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
@RequiresApi(23)
private void updateDrmSessionOrReinitializeCodecV23() throws ExoPlaybackException {
@Nullable FrameworkMediaCrypto sessionMediaCrypto = getFrameworkMediaCrypto(sourceDrmSession);
if (sessionMediaCrypto == null) {
// We'd only expect this to happen if the CDM from which the pending session is obtained needs
// provisioning. This is unlikely to happen (it probably requires a switch from one DRM scheme
// to another, where the new CDM hasn't been used before and needs provisioning). It would be
// possible to handle this case more efficiently (i.e. with a new renderer state that waits
// for provisioning to finish and then calls mediaCrypto.setMediaDrmSession), but the extra
// complexity is not warranted given how unlikely the case is to occur.
reinitializeCodec();
return;
}
if (C.PLAYREADY_UUID.equals(sessionMediaCrypto.uuid)) {
// The PlayReady CDM does not implement setMediaDrmSession.
// TODO: Add API check once [Internal ref: b/128835874] is fixed.
reinitializeCodec();
return;
}
if (flushOrReinitializeCodec()) {
// The codec was reinitialized. The new codec will be using the new DRM session, so there's
// nothing more to do.
return;
}
private void updateDrmSessionV23() throws ExoPlaybackException {
try {
mediaCrypto.setMediaDrmSession(sessionMediaCrypto.sessionId);
mediaCrypto.setMediaDrmSession(getFrameworkMediaCrypto(sourceDrmSession).sessionId);
} catch (MediaCryptoException e) {
throw createRendererException(e, inputFormat);
}
@ -2115,7 +2132,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
if (!batchBuffer.isEmpty() && waitingForFirstSampleInFormat) {
// This is the first buffer in a new format, the output format must be updated.
outputFormat = Assertions.checkNotNull(inputFormat);
outputFormat = checkNotNull(inputFormat);
onOutputFormatChanged(outputFormat, /* mediaFormat= */ null);
waitingForFirstSampleInFormat = false;
}