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
This commit is contained in:
ibaker 2024-01-05 07:42:55 -08:00 committed by Copybara-Service
parent 62f6c64a91
commit 77f311917f
5 changed files with 71 additions and 10 deletions

View File

@ -107,6 +107,15 @@
* Fix `ERROR_DRM_SESSION_NOT_OPENED` when switching from encrypted to * Fix `ERROR_DRM_SESSION_NOT_OPENED` when switching from encrypted to
clear content without a surface attached to the player. The error was clear content without a surface attached to the player. The error was
due to incorrectly using a secure decoder to play the clear content. 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: * Effect:
* Muxers: * Muxers:
* IMA extension: * IMA extension:

View File

@ -25,6 +25,7 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.IntRange; import androidx.annotation.IntRange;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.BundleCollectionUtil; import androidx.media3.common.util.BundleCollectionUtil;
import androidx.media3.common.util.UnstableApi; 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. * @param scheme The {@link UUID} of the protection scheme.
*/ */
@SuppressWarnings("deprecation") // Calling deprecated constructor to reduce code duplication.
public Builder(UUID scheme) { public Builder(UUID scheme) {
this();
this.scheme = scheme; this.scheme = scheme;
this.licenseRequestHeaders = ImmutableMap.of();
this.forcedSessionTrackTypes = ImmutableList.of();
} }
/** /**
@ -653,6 +654,7 @@ public final class MediaItem implements Bundleable {
@Deprecated @Deprecated
private Builder() { private Builder() {
this.licenseRequestHeaders = ImmutableMap.of(); this.licenseRequestHeaders = ImmutableMap.of();
this.playClearContentWithoutKey = true;
this.forcedSessionTrackTypes = ImmutableList.of(); this.forcedSessionTrackTypes = ImmutableList.of();
} }
@ -706,7 +708,11 @@ public final class MediaItem implements Bundleable {
return this; return this;
} }
/** Sets whether multi session is enabled. */ /**
* Sets whether multi session is enabled.
*
* <p>The default is {@code false} (multi session disabled).
*/
@CanIgnoreReturnValue @CanIgnoreReturnValue
public Builder setMultiSession(boolean multiSession) { public Builder setMultiSession(boolean multiSession) {
this.multiSession = 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 * Sets whether to always use the default DRM license server URI even if the media specifies
* its own DRM license server URI. * its own DRM license server URI.
*
* <p>The default is {@code false}.
*/ */
@CanIgnoreReturnValue @CanIgnoreReturnValue
public Builder setForceDefaultLicenseUri(boolean forceDefaultLicenseUri) { 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 * Sets whether clear samples within protected content should be played when keys for the
* encrypted part of the content have yet to be loaded. * encrypted part of the content have yet to be loaded.
*
* <p>The default is {@code true}.
*/ */
@CanIgnoreReturnValue @CanIgnoreReturnValue
public Builder setPlayClearContentWithoutKey(boolean playClearContentWithoutKey) { public Builder setPlayClearContentWithoutKey(boolean playClearContentWithoutKey) {
@ -753,6 +763,8 @@ public final class MediaItem implements Bundleable {
* *
* <p>This method overrides what has been set by previously calling {@link * <p>This method overrides what has been set by previously calling {@link
* #setForcedSessionTrackTypes(List)}. * #setForcedSessionTrackTypes(List)}.
*
* <p>The default is {@code false}.
*/ */
@CanIgnoreReturnValue @CanIgnoreReturnValue
public Builder setForceSessionsForAudioAndVideoTracks( public Builder setForceSessionsForAudioAndVideoTracks(
@ -773,6 +785,8 @@ public final class MediaItem implements Bundleable {
* *
* <p>This method overrides what has been set by previously calling {@link * <p>This method overrides what has been set by previously calling {@link
* #setForceSessionsForAudioAndVideoTracks(boolean)}. * #setForceSessionsForAudioAndVideoTracks(boolean)}.
*
* <p>The default is an empty list (i.e. DRM sessions are not forced for any track type).
*/ */
@CanIgnoreReturnValue @CanIgnoreReturnValue
public Builder setForcedSessionTrackTypes( 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_URI = Util.intToStringMaxRadix(1);
private static final String FIELD_LICENSE_REQUEST_HEADERS = Util.intToStringMaxRadix(2); 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_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_FORCE_DEFAULT_LICENSE_URI = Util.intToStringMaxRadix(5);
private static final String FIELD_FORCED_SESSION_TRACK_TYPES = Util.intToStringMaxRadix(6); private static final String FIELD_FORCED_SESSION_TRACK_TYPES = Util.intToStringMaxRadix(6);
private static final String FIELD_KEY_SET_ID = Util.intToStringMaxRadix(7); private static final String FIELD_KEY_SET_ID = Util.intToStringMaxRadix(7);

View File

@ -101,7 +101,7 @@ public class MediaItemTest {
.setDrmLicenseRequestHeaders(requestHeaders) .setDrmLicenseRequestHeaders(requestHeaders)
.setDrmMultiSession(true) .setDrmMultiSession(true)
.setDrmForceDefaultLicenseUri(true) .setDrmForceDefaultLicenseUri(true)
.setDrmPlayClearContentWithoutKey(true) .setDrmPlayClearContentWithoutKey(false)
.setDrmSessionForClearTypes(ImmutableList.of(C.TRACK_TYPE_AUDIO)) .setDrmSessionForClearTypes(ImmutableList.of(C.TRACK_TYPE_AUDIO))
.setDrmKeySetId(keySetId) .setDrmKeySetId(keySetId)
.setDrmUuid(C.WIDEVINE_UUID) .setDrmUuid(C.WIDEVINE_UUID)
@ -117,7 +117,7 @@ public class MediaItemTest {
.isEqualTo(requestHeaders); .isEqualTo(requestHeaders);
assertThat(mediaItem.localConfiguration.drmConfiguration.multiSession).isTrue(); assertThat(mediaItem.localConfiguration.drmConfiguration.multiSession).isTrue();
assertThat(mediaItem.localConfiguration.drmConfiguration.forceDefaultLicenseUri).isTrue(); assertThat(mediaItem.localConfiguration.drmConfiguration.forceDefaultLicenseUri).isTrue();
assertThat(mediaItem.localConfiguration.drmConfiguration.playClearContentWithoutKey).isTrue(); assertThat(mediaItem.localConfiguration.drmConfiguration.playClearContentWithoutKey).isFalse();
assertThat(mediaItem.localConfiguration.drmConfiguration.sessionForClearTypes) assertThat(mediaItem.localConfiguration.drmConfiguration.sessionForClearTypes)
.containsExactly(C.TRACK_TYPE_AUDIO); .containsExactly(C.TRACK_TYPE_AUDIO);
assertThat(mediaItem.localConfiguration.drmConfiguration.forcedSessionTrackTypes) assertThat(mediaItem.localConfiguration.drmConfiguration.forcedSessionTrackTypes)
@ -139,7 +139,7 @@ public class MediaItemTest {
.setDrmLicenseRequestHeaders(requestHeaders) .setDrmLicenseRequestHeaders(requestHeaders)
.setDrmMultiSession(true) .setDrmMultiSession(true)
.setDrmForceDefaultLicenseUri(true) .setDrmForceDefaultLicenseUri(true)
.setDrmPlayClearContentWithoutKey(true) .setDrmPlayClearContentWithoutKey(false)
.setDrmSessionForClearTypes(Collections.singletonList(C.TRACK_TYPE_AUDIO)) .setDrmSessionForClearTypes(Collections.singletonList(C.TRACK_TYPE_AUDIO))
.setDrmKeySetId(keySetId) .setDrmKeySetId(keySetId)
.setDrmUuid(C.WIDEVINE_UUID) .setDrmUuid(C.WIDEVINE_UUID)
@ -154,7 +154,7 @@ public class MediaItemTest {
assertThat(mediaItem.localConfiguration.drmConfiguration.licenseRequestHeaders).isEmpty(); assertThat(mediaItem.localConfiguration.drmConfiguration.licenseRequestHeaders).isEmpty();
assertThat(mediaItem.localConfiguration.drmConfiguration.multiSession).isFalse(); assertThat(mediaItem.localConfiguration.drmConfiguration.multiSession).isFalse();
assertThat(mediaItem.localConfiguration.drmConfiguration.forceDefaultLicenseUri).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.sessionForClearTypes).isEmpty();
assertThat(mediaItem.localConfiguration.drmConfiguration.forcedSessionTrackTypes).isEmpty(); assertThat(mediaItem.localConfiguration.drmConfiguration.forcedSessionTrackTypes).isEmpty();
assertThat(mediaItem.localConfiguration.drmConfiguration.getKeySetId()).isNull(); assertThat(mediaItem.localConfiguration.drmConfiguration.getKeySetId()).isNull();
@ -249,6 +249,35 @@ public class MediaItemTest {
.build()); .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 @Test
public void createDrmConfigurationInstance_roundTripViaBundle_yieldsEqualInstance() { public void createDrmConfigurationInstance_roundTripViaBundle_yieldsEqualInstance() {
MediaItem.DrmConfiguration drmConfiguration = MediaItem.DrmConfiguration drmConfiguration =

View File

@ -98,17 +98,20 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
* FrameworkMediaDrm#DEFAULT_PROVIDER}. * FrameworkMediaDrm#DEFAULT_PROVIDER}.
* <li>{@link #setMultiSession multiSession}: {@code false}. * <li>{@link #setMultiSession multiSession}: {@code false}.
* <li>{@link #setUseDrmSessionsForClearContent useDrmSessionsForClearContent}: No tracks. * <li>{@link #setUseDrmSessionsForClearContent useDrmSessionsForClearContent}: No tracks.
* <li>{@link #setPlayClearSamplesWithoutKeys playClearSamplesWithoutKeys}: {@code false}. * <li>{@link #setPlayClearSamplesWithoutKeys playClearSamplesWithoutKeys}: {@code true}.
* <li>{@link #setLoadErrorHandlingPolicy LoadErrorHandlingPolicy}: {@link * <li>{@link #setLoadErrorHandlingPolicy LoadErrorHandlingPolicy}: {@link
* DefaultLoadErrorHandlingPolicy}. * DefaultLoadErrorHandlingPolicy}.
* <li>{@link #setSessionKeepaliveMs sessionKeepaliveMs}: {@link
* #DEFAULT_SESSION_KEEPALIVE_MS}.
* </ul> * </ul>
*/ */
public Builder() { public Builder() {
keyRequestParameters = new HashMap<>(); keyRequestParameters = new HashMap<>();
uuid = C.WIDEVINE_UUID; uuid = C.WIDEVINE_UUID;
exoMediaDrmProvider = FrameworkMediaDrm.DEFAULT_PROVIDER; exoMediaDrmProvider = FrameworkMediaDrm.DEFAULT_PROVIDER;
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
useDrmSessionsForClearContentTrackTypes = new int[0]; useDrmSessionsForClearContentTrackTypes = new int[0];
playClearSamplesWithoutKeys = true;
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
sessionKeepaliveMs = DEFAULT_SESSION_KEEPALIVE_MS; sessionKeepaliveMs = DEFAULT_SESSION_KEEPALIVE_MS;
} }

View File

@ -1461,6 +1461,9 @@ public final class DefaultAnalyticsCollectorTest {
DRM_SCHEME_UUID, DRM_SCHEME_UUID,
uuid -> new FakeExoMediaDrm.Builder().setEnforceValidKeyResponses(false).build()) uuid -> new FakeExoMediaDrm.Builder().setEnforceValidKeyResponses(false).build())
.setMultiSession(true) .setMultiSession(true)
// The fake samples are not encrypted, so this forces the test to block playback until
// keys are ready.
.setPlayClearSamplesWithoutKeys(false)
.build(mediaDrmCallback); .build(mediaDrmCallback);
MediaSource mediaSource = MediaSource mediaSource =
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, failingDrmSessionManager, VIDEO_FORMAT_DRM_1); new FakeMediaSource(SINGLE_PERIOD_TIMELINE, failingDrmSessionManager, VIDEO_FORMAT_DRM_1);