From 77f311917fcb0844b74b162acddf59b22be8abc0 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 5 Jan 2024 07:42:55 -0800 Subject: [PATCH] Play clear samples in DRM content without keys by default This behavior was previously available as opt-in via `MediaItem.DrmConfiguration.Builder.setPlayClearContentWithoutKey` and `DefaultDrmSessionManager.Builder.setPlayClearSamplesWithoutKeys`. This change flips the default of both these properties to true. This should speed up the time for playback to start when playing DRM content with a 'clear lead' of unencrypted samples at the start. Previously playback would wait until the keys for the later encrypted samples were ready. The new behaviour could result in mid-playback stalls/rebuffers if the keys are not ready yet by the transition from clear to encrypted samples, but this is not really a regression since previously playback wouldn't have started at all at this point. PiperOrigin-RevId: 595992727 --- RELEASENOTES.md | 9 +++++ .../androidx/media3/common/MediaItem.java | 25 +++++++++++-- .../androidx/media3/common/MediaItemTest.java | 37 +++++++++++++++++-- .../drm/DefaultDrmSessionManager.java | 7 +++- .../DefaultAnalyticsCollectorTest.java | 3 ++ 5 files changed, 71 insertions(+), 10 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7980e4e0f1..b87e86decf 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -107,6 +107,15 @@ * Fix `ERROR_DRM_SESSION_NOT_OPENED` when switching from encrypted to clear content without a surface attached to the player. The error was due to incorrectly using a secure decoder to play the clear content. + * Play 'clear lead' unencrypted samples in DRM content immediately by + default, even if the keys for the later encrypted samples aren't ready + yet. This may lead to mid-playback stalls if the keys still aren't ready + when the playback position reaches the encrypted samples (but previously + playback wouldn't have started at all by this point). This behavior can + be disabled with + [`MediaItem.DrmConfiguration.Builder.setPlayClearContentWithoutKey`](https://developer.android.com/reference/androidx/media3/common/MediaItem.DrmConfiguration.Builder#setPlayClearContentWithoutKey\(boolean\)) + or + [`DefaultDrmSessionManager.Builder.setPlayClearSamplesWithoutKeys`](https://developer.android.com/reference/androidx/media3/exoplayer/drm/DefaultDrmSessionManager.Builder#setPlayClearSamplesWithoutKeys\(boolean\)). * Effect: * Muxers: * IMA extension: diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaItem.java b/libraries/common/src/main/java/androidx/media3/common/MediaItem.java index bef87eb64e..aaa6478295 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MediaItem.java +++ b/libraries/common/src/main/java/androidx/media3/common/MediaItem.java @@ -25,6 +25,7 @@ import android.net.Uri; import android.os.Bundle; import androidx.annotation.IntRange; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.BundleCollectionUtil; import androidx.media3.common.util.UnstableApi; @@ -640,10 +641,10 @@ public final class MediaItem implements Bundleable { * * @param scheme The {@link UUID} of the protection scheme. */ + @SuppressWarnings("deprecation") // Calling deprecated constructor to reduce code duplication. public Builder(UUID scheme) { + this(); this.scheme = scheme; - this.licenseRequestHeaders = ImmutableMap.of(); - this.forcedSessionTrackTypes = ImmutableList.of(); } /** @@ -653,6 +654,7 @@ public final class MediaItem implements Bundleable { @Deprecated private Builder() { this.licenseRequestHeaders = ImmutableMap.of(); + this.playClearContentWithoutKey = true; this.forcedSessionTrackTypes = ImmutableList.of(); } @@ -706,7 +708,11 @@ public final class MediaItem implements Bundleable { return this; } - /** Sets whether multi session is enabled. */ + /** + * Sets whether multi session is enabled. + * + *

The default is {@code false} (multi session disabled). + */ @CanIgnoreReturnValue public Builder setMultiSession(boolean multiSession) { this.multiSession = multiSession; @@ -716,6 +722,8 @@ public final class MediaItem implements Bundleable { /** * Sets whether to always use the default DRM license server URI even if the media specifies * its own DRM license server URI. + * + *

The default is {@code false}. */ @CanIgnoreReturnValue public Builder setForceDefaultLicenseUri(boolean forceDefaultLicenseUri) { @@ -726,6 +734,8 @@ public final class MediaItem implements Bundleable { /** * Sets whether clear samples within protected content should be played when keys for the * encrypted part of the content have yet to be loaded. + * + *

The default is {@code true}. */ @CanIgnoreReturnValue public Builder setPlayClearContentWithoutKey(boolean playClearContentWithoutKey) { @@ -753,6 +763,8 @@ public final class MediaItem implements Bundleable { * *

This method overrides what has been set by previously calling {@link * #setForcedSessionTrackTypes(List)}. + * + *

The default is {@code false}. */ @CanIgnoreReturnValue public Builder setForceSessionsForAudioAndVideoTracks( @@ -773,6 +785,8 @@ public final class MediaItem implements Bundleable { * *

This method overrides what has been set by previously calling {@link * #setForceSessionsForAudioAndVideoTracks(boolean)}. + * + *

The default is an empty list (i.e. DRM sessions are not forced for any track type). */ @CanIgnoreReturnValue public Builder setForcedSessionTrackTypes( @@ -917,7 +931,10 @@ public final class MediaItem implements Bundleable { private static final String FIELD_LICENSE_URI = Util.intToStringMaxRadix(1); private static final String FIELD_LICENSE_REQUEST_HEADERS = Util.intToStringMaxRadix(2); private static final String FIELD_MULTI_SESSION = Util.intToStringMaxRadix(3); - private static final String FIELD_PLAY_CLEAR_CONTENT_WITHOUT_KEY = Util.intToStringMaxRadix(4); + + @VisibleForTesting + static final String FIELD_PLAY_CLEAR_CONTENT_WITHOUT_KEY = Util.intToStringMaxRadix(4); + private static final String FIELD_FORCE_DEFAULT_LICENSE_URI = Util.intToStringMaxRadix(5); private static final String FIELD_FORCED_SESSION_TRACK_TYPES = Util.intToStringMaxRadix(6); private static final String FIELD_KEY_SET_ID = Util.intToStringMaxRadix(7); diff --git a/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java b/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java index ba4e3feac3..81c1771ffb 100644 --- a/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java @@ -101,7 +101,7 @@ public class MediaItemTest { .setDrmLicenseRequestHeaders(requestHeaders) .setDrmMultiSession(true) .setDrmForceDefaultLicenseUri(true) - .setDrmPlayClearContentWithoutKey(true) + .setDrmPlayClearContentWithoutKey(false) .setDrmSessionForClearTypes(ImmutableList.of(C.TRACK_TYPE_AUDIO)) .setDrmKeySetId(keySetId) .setDrmUuid(C.WIDEVINE_UUID) @@ -117,7 +117,7 @@ public class MediaItemTest { .isEqualTo(requestHeaders); assertThat(mediaItem.localConfiguration.drmConfiguration.multiSession).isTrue(); assertThat(mediaItem.localConfiguration.drmConfiguration.forceDefaultLicenseUri).isTrue(); - assertThat(mediaItem.localConfiguration.drmConfiguration.playClearContentWithoutKey).isTrue(); + assertThat(mediaItem.localConfiguration.drmConfiguration.playClearContentWithoutKey).isFalse(); assertThat(mediaItem.localConfiguration.drmConfiguration.sessionForClearTypes) .containsExactly(C.TRACK_TYPE_AUDIO); assertThat(mediaItem.localConfiguration.drmConfiguration.forcedSessionTrackTypes) @@ -139,7 +139,7 @@ public class MediaItemTest { .setDrmLicenseRequestHeaders(requestHeaders) .setDrmMultiSession(true) .setDrmForceDefaultLicenseUri(true) - .setDrmPlayClearContentWithoutKey(true) + .setDrmPlayClearContentWithoutKey(false) .setDrmSessionForClearTypes(Collections.singletonList(C.TRACK_TYPE_AUDIO)) .setDrmKeySetId(keySetId) .setDrmUuid(C.WIDEVINE_UUID) @@ -154,7 +154,7 @@ public class MediaItemTest { assertThat(mediaItem.localConfiguration.drmConfiguration.licenseRequestHeaders).isEmpty(); assertThat(mediaItem.localConfiguration.drmConfiguration.multiSession).isFalse(); assertThat(mediaItem.localConfiguration.drmConfiguration.forceDefaultLicenseUri).isFalse(); - assertThat(mediaItem.localConfiguration.drmConfiguration.playClearContentWithoutKey).isFalse(); + assertThat(mediaItem.localConfiguration.drmConfiguration.playClearContentWithoutKey).isTrue(); assertThat(mediaItem.localConfiguration.drmConfiguration.sessionForClearTypes).isEmpty(); assertThat(mediaItem.localConfiguration.drmConfiguration.forcedSessionTrackTypes).isEmpty(); assertThat(mediaItem.localConfiguration.drmConfiguration.getKeySetId()).isNull(); @@ -249,6 +249,35 @@ public class MediaItemTest { .build()); } + @Test + public void createDefaultDrmConfigurationInstance_roundTripViaBundle_yieldsEqualInstance() { + MediaItem.DrmConfiguration drmConfiguration = + new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID).build(); + + MediaItem.DrmConfiguration drmConfigurationFromBundle = + MediaItem.DrmConfiguration.fromBundle(drmConfiguration.toBundle()); + + assertThat(drmConfigurationFromBundle).isEqualTo(drmConfiguration); + } + + @Test + public void drmConfigurationFromOldBundle_yieldsIntendedInstance() { + MediaItem.DrmConfiguration drmConfiguration = + new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID).build(); + + Bundle bundle = drmConfiguration.toBundle(); + // Remove the playClearSamplesWithoutKey field, to simulate a 'default' bundle from an old + // version of the library, and check the result is 'false' (as intended by the old library). + bundle.remove(MediaItem.DrmConfiguration.FIELD_PLAY_CLEAR_CONTENT_WITHOUT_KEY); + + MediaItem.DrmConfiguration drmConfigurationFromBundle = + MediaItem.DrmConfiguration.fromBundle(bundle); + + MediaItem.DrmConfiguration expectedDrmConfiguration = + drmConfiguration.buildUpon().setPlayClearContentWithoutKey(false).build(); + assertThat(drmConfigurationFromBundle).isEqualTo(expectedDrmConfiguration); + } + @Test public void createDrmConfigurationInstance_roundTripViaBundle_yieldsEqualInstance() { MediaItem.DrmConfiguration drmConfiguration = diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManager.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManager.java index 5067c57319..f3db12b3df 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManager.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManager.java @@ -98,17 +98,20 @@ public class DefaultDrmSessionManager implements DrmSessionManager { * FrameworkMediaDrm#DEFAULT_PROVIDER}. *

  • {@link #setMultiSession multiSession}: {@code false}. *
  • {@link #setUseDrmSessionsForClearContent useDrmSessionsForClearContent}: No tracks. - *
  • {@link #setPlayClearSamplesWithoutKeys playClearSamplesWithoutKeys}: {@code false}. + *
  • {@link #setPlayClearSamplesWithoutKeys playClearSamplesWithoutKeys}: {@code true}. *
  • {@link #setLoadErrorHandlingPolicy LoadErrorHandlingPolicy}: {@link * DefaultLoadErrorHandlingPolicy}. + *
  • {@link #setSessionKeepaliveMs sessionKeepaliveMs}: {@link + * #DEFAULT_SESSION_KEEPALIVE_MS}. * */ public Builder() { keyRequestParameters = new HashMap<>(); uuid = C.WIDEVINE_UUID; exoMediaDrmProvider = FrameworkMediaDrm.DEFAULT_PROVIDER; - loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); useDrmSessionsForClearContentTrackTypes = new int[0]; + playClearSamplesWithoutKeys = true; + loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); sessionKeepaliveMs = DEFAULT_SESSION_KEEPALIVE_MS; } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java index 72cbc60b29..f4f2f3f3a7 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/DefaultAnalyticsCollectorTest.java @@ -1461,6 +1461,9 @@ public final class DefaultAnalyticsCollectorTest { DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm.Builder().setEnforceValidKeyResponses(false).build()) .setMultiSession(true) + // The fake samples are not encrypted, so this forces the test to block playback until + // keys are ready. + .setPlayClearSamplesWithoutKeys(false) .build(mediaDrmCallback); MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, failingDrmSessionManager, VIDEO_FORMAT_DRM_1);