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}. *