From 8716c0ee32a6c5399d8f2e6b3e4f14563bb14c7f Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 24 Jan 2018 06:17:18 -0800 Subject: [PATCH] Implement a best-effort DRM session acquisition approach Try to delay failure for as long as possible. That is, propagate DRM session failures only after an encrypted buffer arrives or clear sample playback without session is not allowed. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=183076348 --- RELEASENOTES.md | 4 +- .../ext/vp9/LibvpxVideoRenderer.java | 8 ++- .../com/google/android/exoplayer2/Format.java | 44 +------------ .../audio/SimpleDecoderAudioRenderer.java | 8 ++- .../drm/DefaultDrmSessionManager.java | 39 ++++++++--- .../android/exoplayer2/drm/DrmInitData.java | 65 +++++++++++++++++++ .../mediacodec/MediaCodecRenderer.java | 13 ++-- 7 files changed, 117 insertions(+), 64 deletions(-) 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) {