diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c624cce9968..de60f33c02d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -46,7 +46,9 @@ positions. * Note: `SeekParameters` are not currently supported when playing HLS streams. * DRM: Optimistically attempt playback of DRM protected content that does not - declare scheme specific init data + declare scheme specific init data in the manifest. If playback of clear + samples without keys is allowed, delay DRM session error propagation until + keys are actually needed ([#3630](https://github.com/google/ExoPlayer/issues/3630)). * DASH: * Support in-band Emsg events targeting player with scheme id diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index 108a89f56a6..5a2093b2b7a 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -647,10 +647,12 @@ private void maybeInitDecoder() throws ExoPlaybackException { if (mediaCrypto == null) { DrmSessionException drmError = drmSession.getError(); if (drmError != null) { - throw ExoPlaybackException.createForRenderer(drmError, getIndex()); + // Continue for now. We may be able to avoid failure if the session recovers, or if a new + // input format causes the session to be replaced before it's used. + } else { + // The drm session isn't open yet. + return; } - // The drm session isn't open yet. - return; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index 6ef57537f10..c830a246ae7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -21,7 +21,6 @@ import android.os.Parcel; import android.os.Parcelable; import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -502,8 +501,8 @@ public Format copyWithManifestFormatInfo(Format manifestFormat) { float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate; @C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags; String language = this.language == null ? manifestFormat.language : this.language; - DrmInitData drmInitData = manifestFormat.drmInitData != null - ? getFilledManifestDrmData(manifestFormat.drmInitData) : this.drmInitData; + DrmInitData drmInitData = + DrmInitData.createSessionCreationData(manifestFormat.drmInitData, this.drmInitData); return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, @@ -768,43 +767,4 @@ public Format[] newArray(int size) { } }; - - private DrmInitData getFilledManifestDrmData(DrmInitData manifestDrmData) { - // All exposed SchemeDatas must include key request information. - ArrayList exposedSchemeDatas = new ArrayList<>(); - ArrayList emptySchemeDatas = new ArrayList<>(); - for (int i = 0; i < manifestDrmData.schemeDataCount; i++) { - SchemeData schemeData = manifestDrmData.get(i); - if (schemeData.hasData()) { - exposedSchemeDatas.add(schemeData); - } else /* needs initialization data filling */ { - emptySchemeDatas.add(schemeData); - } - } - - if (emptySchemeDatas.isEmpty()) { - // Manifest DRM information is complete. - return manifestDrmData; - } else if (drmInitData == null) { - // The manifest DRM data needs filling but this format does not include enough information to - // do it. A subset of the manifest's scheme datas should not be exposed because a - // DrmSessionManager could decide it does not support the format, while the missing - // information comes in a format feed immediately after. - return null; - } - - int needFillingCount = emptySchemeDatas.size(); - for (int i = 0; i < drmInitData.schemeDataCount; i++) { - SchemeData mediaSchemeData = drmInitData.get(i); - for (int j = 0; j < needFillingCount; j++) { - if (mediaSchemeData.canReplace(emptySchemeDatas.get(j))) { - exposedSchemeDatas.add(mediaSchemeData); - break; - } - } - } - return exposedSchemeDatas.isEmpty() ? null : new DrmInitData(manifestDrmData.schemeType, - exposedSchemeDatas.toArray(new SchemeData[exposedSchemeDatas.size()])); - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 397c5aaad75..eda8cfb15d6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -566,10 +566,12 @@ private void maybeInitDecoder() throws ExoPlaybackException { if (mediaCrypto == null) { DrmSessionException drmError = drmSession.getError(); if (drmError != null) { - throw ExoPlaybackException.createForRenderer(drmError, getIndex()); + // Continue for now. We may be able to avoid failure if the session recovers, or if a new + // input format causes the session to be replaced before it's used. + } else { + // The drm session isn't open yet. + return; } - // The drm session isn't open yet. - return; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 6a5185a2661..d8cbd00f775 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -23,6 +23,7 @@ import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.text.TextUtils; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.drm.DefaultDrmSession.ProvisioningManager; @@ -83,6 +84,17 @@ public interface EventListener { } + /** + * Signals that the {@link DrmInitData} passed to {@link #acquireSession} does not contain does + * not contain scheme data for the required UUID. + */ + public static final class MissingSchemeDataException extends Exception { + + private MissingSchemeDataException(UUID uuid) { + super("Media does not support uuid: " + uuid); + } + } + /** * The key to use when passing CustomData to a PlayReady instance in an optional parameter map. */ @@ -108,6 +120,7 @@ public interface EventListener { /** Number of times to retry for initial provisioning and key request for reporting error. */ public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3; + private static final String TAG = "DefaultDrmSessionMgr"; private static final String CENC_SCHEME_MIME_TYPE = "cenc"; private final UUID uuid; @@ -351,8 +364,14 @@ public void setMode(@Mode int mode, byte[] offlineLicenseKeySetId) { public boolean canAcquireSession(@NonNull DrmInitData drmInitData) { SchemeData schemeData = getSchemeData(drmInitData, uuid, true); if (schemeData == null) { - // No data for this manager's scheme. - return false; + if (drmInitData.schemeDataCount == 1 && drmInitData.get(0).matches(C.COMMON_PSSH_UUID)) { + // Assume scheme specific data will be added before the session is opened. + Log.w( + TAG, "DrmInitData only contains common PSSH SchemeData. Assuming support for: " + uuid); + } else { + // No data for this manager's scheme. + return false; + } } String schemeType = drmInitData.schemeType; if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) { @@ -382,15 +401,15 @@ public DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitDa if (offlineLicenseKeySetId == null) { SchemeData data = getSchemeData(drmInitData, uuid, false); if (data == null) { - final IllegalStateException error = new IllegalStateException( - "Media does not support uuid: " + uuid); + final MissingSchemeDataException error = new MissingSchemeDataException(uuid); if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrmSessionManagerError(error); - } - }); + eventHandler.post( + new Runnable() { + @Override + public void run() { + eventListener.onDrmSessionManagerError(error); + } + }); } return new ErrorStateDrmSession<>(new DrmSessionException(error)); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index 73b443dcecc..5662730650f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; @@ -32,6 +33,58 @@ */ public final class DrmInitData implements Comparator, Parcelable { + /** + * Merges {@link DrmInitData} obtained from a media manifest and a media stream. + * + *

The result is generated as follows. + * + *

    + *
      + * Include all {@link SchemeData}s from {@code manifestData} where {@link + * SchemeData#hasData()} is true. + *
    + *
      + * Include all {@link SchemeData}s in {@code mediaData} where {@link SchemeData#hasData()} is + * true and for which we did not include an entry from the manifest targeting the same UUID. + *
    + *
      + * If available, the scheme type from the manifest is used. If not, the scheme type from the + * media is used. + *
    + *
+ * + * @param manifestData DRM session acquisition data obtained from the manifest. + * @param mediaData DRM session acquisition data obtained from the media. + * @return A {@link DrmInitData} obtained from merging a media manifest and a media stream. + */ + public static @Nullable DrmInitData createSessionCreationData( + @Nullable DrmInitData manifestData, @Nullable DrmInitData mediaData) { + ArrayList result = new ArrayList<>(); + String schemeType = null; + if (manifestData != null) { + schemeType = manifestData.schemeType; + for (SchemeData data : manifestData.schemeDatas) { + if (data.hasData()) { + result.add(data); + } + } + } + + if (mediaData != null) { + if (schemeType == null) { + schemeType = mediaData.schemeType; + } + int manifestDatasCount = result.size(); + for (SchemeData data : mediaData.schemeDatas) { + if (data.hasData() && !containsSchemeDataWithUuid(result, manifestDatasCount, data.uuid)) { + result.add(data); + } + } + } + + return result.isEmpty() ? null : new DrmInitData(schemeType, result); + } + private final SchemeData[] schemeDatas; // Lazily initialized hashcode. @@ -193,6 +246,18 @@ public DrmInitData[] newArray(int size) { }; + // Internal methods. + + private static boolean containsSchemeDataWithUuid( + ArrayList datas, int limit, UUID uuid) { + for (int i = 0; i < limit; i++) { + if (datas.get(i).uuid.equals(uuid)) { + return true; + } + } + return false; + } + /** * Scheme initialization data. */ 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 4b1af7e385b..99d96865af2 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 @@ -338,13 +338,16 @@ protected final void maybeInitCodec() throws ExoPlaybackException { if (mediaCrypto == null) { DrmSessionException drmError = drmSession.getError(); if (drmError != null) { - throw ExoPlaybackException.createForRenderer(drmError, getIndex()); + // Continue for now. We may be able to avoid failure if the session recovers, or if a new + // input format causes the session to be replaced before it's used. + } else { + // The drm session isn't open yet. + return; } - // The drm session isn't open yet. - return; + } else { + wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto(); + drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType); } - wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto(); - drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType); } if (codecInfo == null) {