diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 814b839fd8..5c02cbc567 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index bb3ad910f0..0cec4ab789 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -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; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index 1706afcb35..e72d552a68 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -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. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java index 4253d3011c..068f1b3782 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java @@ -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() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 6648ae5fb2..a29de96ceb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -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; }