diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d14bd60020d..6c377bd1a62 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,9 @@ * ExoPlayer: * Transformer: * Track Selection: + * Add `DefaultTrackSelector.Parameters.allowAudioNonSeamlessAdaptiveness` + to explicitly allow or disallow non-seamless adaptation. The default + stays at its current behavior of `true`. * Extractors: * Audio: * Video: diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java index 53bc7deda56..a0dacd86e69 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java @@ -386,7 +386,9 @@ public ParametersBuilder setExceedAudioConstraintsIfNecessary( /** * Sets whether to allow adaptive audio selections containing mixed MIME types. * - *

Adaptations between different MIME types may not be completely seamless. + *

Adaptations between different MIME types may not be completely seamless, in which case + * {@link Parameters.Builder#setAllowAudioNonSeamlessAdaptiveness(boolean)} also needs to be + * {@code true} for mixed MIME type selections to be made. * * @param allowAudioMixedMimeTypeAdaptiveness Whether to allow adaptive audio selections * containing mixed MIME types. @@ -769,6 +771,7 @@ public static final class Builder extends TrackSelectionParameters.Builder { private boolean allowAudioMixedSampleRateAdaptiveness; private boolean allowAudioMixedChannelCountAdaptiveness; private boolean allowAudioMixedDecoderSupportAdaptiveness; + private boolean allowAudioNonSeamlessAdaptiveness; private boolean constrainAudioChannelCountToDeviceCapabilities; // General private boolean exceedRendererCapabilitiesIfNecessary; @@ -825,6 +828,7 @@ private Builder(Parameters initialValues) { initialValues.allowAudioMixedChannelCountAdaptiveness; allowAudioMixedDecoderSupportAdaptiveness = initialValues.allowAudioMixedDecoderSupportAdaptiveness; + allowAudioNonSeamlessAdaptiveness = initialValues.allowAudioNonSeamlessAdaptiveness; constrainAudioChannelCountToDeviceCapabilities = initialValues.constrainAudioChannelCountToDeviceCapabilities; // General @@ -881,6 +885,10 @@ private Builder(Bundle bundle) { bundle.getBoolean( Parameters.FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS, defaultValue.allowAudioMixedDecoderSupportAdaptiveness)); + setAllowAudioNonSeamlessAdaptiveness( + bundle.getBoolean( + Parameters.FIELD_ALLOW_AUDIO_NON_SEAMLESS_ADAPTIVENESS, + defaultValue.allowAudioNonSeamlessAdaptiveness)); setConstrainAudioChannelCountToDeviceCapabilities( bundle.getBoolean( Parameters.FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES, @@ -1136,7 +1144,9 @@ public Builder setExceedAudioConstraintsIfNecessary( /** * Sets whether to allow adaptive audio selections containing mixed MIME types. * - *

Adaptations between different MIME types may not be completely seamless. + *

Adaptations between different MIME types may not be completely seamless, in which case + * {@link #setAllowAudioNonSeamlessAdaptiveness(boolean)} also needs to be {@code true} for + * mixed MIME type selections to be made. * * @param allowAudioMixedMimeTypeAdaptiveness Whether to allow adaptive audio selections * containing mixed MIME types. @@ -1211,6 +1221,21 @@ public Builder setPreferredAudioMimeTypes(String... mimeTypes) { return this; } + /** + * Sets whether to allow adaptive audio selections where adaptation may not be completely + * seamless. + * + * @param allowAudioNonSeamlessAdaptiveness Whether to allow adaptive audio selections where + * adaptation may not be completely seamless. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setAllowAudioNonSeamlessAdaptiveness( + boolean allowAudioNonSeamlessAdaptiveness) { + this.allowAudioNonSeamlessAdaptiveness = allowAudioNonSeamlessAdaptiveness; + return this; + } + /** * Whether to only select audio tracks with channel counts that don't exceed the device's * output capabilities. The default value is {@code true}. @@ -1580,6 +1605,7 @@ private void init(Builder this) { allowAudioMixedSampleRateAdaptiveness = false; allowAudioMixedChannelCountAdaptiveness = false; allowAudioMixedDecoderSupportAdaptiveness = false; + allowAudioNonSeamlessAdaptiveness = true; constrainAudioChannelCountToDeviceCapabilities = true; // General exceedRendererCapabilitiesIfNecessary = true; @@ -1713,7 +1739,9 @@ public static Parameters getDefaults(Context context) { /** * Whether to allow adaptive audio selections containing mixed MIME types. Adaptations between - * different MIME types may not be completely seamless. The default value is {@code false}. + * different MIME types may not be completely seamless, in which case {@link + * #allowAudioNonSeamlessAdaptiveness} also needs to be {@code true} for mixed MIME type + * selections to be made. The default value is {@code false}. */ public final boolean allowAudioMixedMimeTypeAdaptiveness; @@ -1737,6 +1765,12 @@ public static Parameters getDefaults(Context context) { */ public final boolean allowAudioMixedDecoderSupportAdaptiveness; + /** + * Whether to allow adaptive audio selections where adaptation may not be completely seamless. + * The default value is {@code true}. + */ + public final boolean allowAudioNonSeamlessAdaptiveness; + /** * Whether to constrain audio track selection so that the selected track's channel count does * not exceed the device's output capabilities. The default value is {@code true}. @@ -1793,6 +1827,7 @@ private Parameters(Builder builder) { allowAudioMixedSampleRateAdaptiveness = builder.allowAudioMixedSampleRateAdaptiveness; allowAudioMixedChannelCountAdaptiveness = builder.allowAudioMixedChannelCountAdaptiveness; allowAudioMixedDecoderSupportAdaptiveness = builder.allowAudioMixedDecoderSupportAdaptiveness; + allowAudioNonSeamlessAdaptiveness = builder.allowAudioNonSeamlessAdaptiveness; constrainAudioChannelCountToDeviceCapabilities = builder.constrainAudioChannelCountToDeviceCapabilities; // General @@ -1885,6 +1920,7 @@ public boolean equals(@Nullable Object obj) { == other.allowAudioMixedChannelCountAdaptiveness && allowAudioMixedDecoderSupportAdaptiveness == other.allowAudioMixedDecoderSupportAdaptiveness + && allowAudioNonSeamlessAdaptiveness == other.allowAudioNonSeamlessAdaptiveness && constrainAudioChannelCountToDeviceCapabilities == other.constrainAudioChannelCountToDeviceCapabilities // General @@ -1913,6 +1949,7 @@ public int hashCode() { result = 31 * result + (allowAudioMixedSampleRateAdaptiveness ? 1 : 0); result = 31 * result + (allowAudioMixedChannelCountAdaptiveness ? 1 : 0); result = 31 * result + (allowAudioMixedDecoderSupportAdaptiveness ? 1 : 0); + result = 31 * result + (allowAudioNonSeamlessAdaptiveness ? 1 : 0); result = 31 * result + (constrainAudioChannelCountToDeviceCapabilities ? 1 : 0); // General result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0); @@ -1961,6 +1998,8 @@ public int hashCode() { Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 16); private static final String FIELD_ALLOW_INVALIDATE_SELECTIONS_ON_RENDERER_CAPABILITIES_CHANGE = Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 17); + private static final String FIELD_ALLOW_AUDIO_NON_SEAMLESS_ADAPTIVENESS = + Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 18); @Override public Bundle toBundle() { @@ -1989,6 +2028,8 @@ public Bundle toBundle() { bundle.putBoolean( FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS, allowAudioMixedDecoderSupportAdaptiveness); + bundle.putBoolean( + FIELD_ALLOW_AUDIO_NON_SEAMLESS_ADAPTIVENESS, allowAudioNonSeamlessAdaptiveness); bundle.putBoolean( FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES, constrainAudioChannelCountToDeviceCapabilities); @@ -2678,7 +2719,8 @@ protected Pair selectAudioTrack( params, support, hasVideoRendererWithMappedTracksFinal, - this::isAudioFormatWithinAudioChannelCountConstraints), + this::isAudioFormatWithinAudioChannelCountConstraints, + rendererMixedMimeTypeAdaptationSupports[rendererIndex]), AudioTrackInfo::compareSelections); } @@ -3348,7 +3390,7 @@ public static ImmutableList createForTrackGroup( TrackGroup trackGroup, Parameters params, @Capabilities int[] formatSupport, - @AdaptiveSupport int mixedMimeTypeAdaptionSupport) { + @AdaptiveSupport int mixedMimeTypeAdaptationSupport) { int maxPixelsToRetainForViewport = getMaxVideoPixelsToRetainForViewport( trackGroup, @@ -3368,7 +3410,7 @@ public static ImmutableList createForTrackGroup( /* trackIndex= */ i, params, formatSupport[i], - mixedMimeTypeAdaptionSupport, + mixedMimeTypeAdaptationSupport, isSuitableForViewport)); } return listBuilder.build(); @@ -3482,7 +3524,7 @@ public boolean isCompatibleForAdaptationWith(VideoTrackInfo otherTrack) { && format.bitrate != Format.NO_VALUE && !parameters.forceHighestSupportedBitrate && !parameters.forceLowestBitrate - && ((rendererSupport & requiredAdaptiveSupport) != 0) + && (rendererSupport & requiredAdaptiveSupport) != 0 ? SELECTION_ELIGIBILITY_ADAPTIVE : SELECTION_ELIGIBILITY_FIXED; } @@ -3562,7 +3604,8 @@ public static ImmutableList createForTrackGroup( Parameters params, @Capabilities int[] formatSupport, boolean hasMappedVideoTracks, - Predicate withinAudioChannelCountConstraints) { + Predicate withinAudioChannelCountConstraints, + @AdaptiveSupport int mixedMimeTypeAdaptationSupport) { ImmutableList.Builder listBuilder = ImmutableList.builder(); for (int i = 0; i < trackGroup.length; i++) { listBuilder.add( @@ -3573,7 +3616,8 @@ public static ImmutableList createForTrackGroup( params, formatSupport[i], hasMappedVideoTracks, - withinAudioChannelCountConstraints)); + withinAudioChannelCountConstraints, + mixedMimeTypeAdaptationSupport)); } return listBuilder.build(); } @@ -3586,6 +3630,7 @@ public static ImmutableList createForTrackGroup( private final int preferredLanguageScore; private final int preferredLanguageIndex; private final int preferredRoleFlagsScore; + private final boolean allowMixedMimeTypes; private final boolean hasMainOrNoRoleFlag; private final int localeLanguageMatchIndex; private final int localeLanguageScore; @@ -3604,9 +3649,19 @@ public AudioTrackInfo( Parameters parameters, @Capabilities int formatSupport, boolean hasMappedVideoTracks, - Predicate withinAudioChannelCountConstraints) { + Predicate withinAudioChannelCountConstraints, + @AdaptiveSupport int mixedMimeTypeAdaptationSupport) { super(rendererIndex, trackGroup, trackIndex); this.parameters = parameters; + @SuppressLint("WrongConstant") + int requiredAdaptiveSupport = + parameters.allowAudioNonSeamlessAdaptiveness + ? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS + | RendererCapabilities.ADAPTIVE_SEAMLESS) + : RendererCapabilities.ADAPTIVE_SEAMLESS; + allowMixedMimeTypes = + parameters.allowAudioMixedMimeTypeAdaptiveness + && (mixedMimeTypeAdaptationSupport & requiredAdaptiveSupport) != 0; this.language = normalizeUndeterminedLanguageToNull(format.language); isWithinRendererCapabilities = isSupported(formatSupport, /* allowExceedsCapabilities= */ false); @@ -3669,7 +3724,8 @@ public AudioTrackInfo( RendererCapabilities.getHardwareAccelerationSupport(formatSupport) == RendererCapabilities.HARDWARE_ACCELERATION_SUPPORTED; selectionEligibility = - evaluateSelectionEligibility(parameters, formatSupport, hasMappedVideoTracks); + evaluateSelectionEligibility( + parameters, formatSupport, hasMappedVideoTracks, requiredAdaptiveSupport); } @Override @@ -3682,7 +3738,7 @@ public boolean isCompatibleForAdaptationWith(AudioTrackInfo otherTrack) { return (parameters.allowAudioMixedChannelCountAdaptiveness || (format.channelCount != Format.NO_VALUE && format.channelCount == otherTrack.format.channelCount)) - && (parameters.allowAudioMixedMimeTypeAdaptiveness + && (allowMixedMimeTypes || (format.sampleMimeType != null && TextUtils.equals(format.sampleMimeType, otherTrack.format.sampleMimeType))) && (parameters.allowAudioMixedSampleRateAdaptiveness @@ -3743,7 +3799,10 @@ public int compareTo(AudioTrackInfo other) { } private @SelectionEligibility int evaluateSelectionEligibility( - Parameters params, @Capabilities int rendererSupport, boolean hasMappedVideoTracks) { + Parameters params, + @Capabilities int rendererSupport, + boolean hasMappedVideoTracks, + @AdaptiveSupport int requiredAdaptiveSupport) { if (!isSupported(rendererSupport, parameters.exceedRendererCapabilitiesIfNecessary)) { return SELECTION_ELIGIBILITY_NO; } @@ -3761,6 +3820,7 @@ public int compareTo(AudioTrackInfo other) { && !parameters.forceLowestBitrate && (parameters.allowMultipleAdaptiveSelections || !hasMappedVideoTracks) && params.audioOffloadPreferences.audioOffloadMode != AUDIO_OFFLOAD_MODE_REQUIRED + && (rendererSupport & requiredAdaptiveSupport) != 0 ? SELECTION_ELIGIBILITY_ADAPTIVE : SELECTION_ELIGIBILITY_FIXED; } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java index dabd6d09412..3c1cf49c860 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelectorTest.java @@ -22,6 +22,8 @@ import static androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED; import static androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_REQUIRED; import static androidx.media3.exoplayer.RendererCapabilities.ADAPTIVE_NOT_SEAMLESS; +import static androidx.media3.exoplayer.RendererCapabilities.ADAPTIVE_NOT_SUPPORTED; +import static androidx.media3.exoplayer.RendererCapabilities.ADAPTIVE_SEAMLESS; import static androidx.media3.exoplayer.RendererCapabilities.AUDIO_OFFLOAD_GAPLESS_SUPPORTED; import static androidx.media3.exoplayer.RendererCapabilities.AUDIO_OFFLOAD_SUPPORTED; import static androidx.media3.exoplayer.RendererCapabilities.DECODER_SUPPORT_FALLBACK; @@ -1860,6 +1862,94 @@ public void selectTracksWithMultipleAudioTracksWithMixedDecoderSupportLevels() t assertAdaptiveSelection(result.selections[0], trackGroups.get(0), 3, 2, 0); } + @Test + public void selectTracks_withMultipleAudioTracksWithoutAdaptationSupport_selectsFixed() + throws Exception { + FakeRendererCapabilities seamlessAudioCapabilities = + new FakeRendererCapabilities( + C.TRACK_TYPE_AUDIO, + RendererCapabilities.create( + FORMAT_HANDLED, ADAPTIVE_NOT_SUPPORTED, TUNNELING_NOT_SUPPORTED)); + + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + TrackGroupArray trackGroups = + singleTrackGroup(formatBuilder.setId("0").build(), formatBuilder.setId("1").build()); + // Explicitly allow non-seamless adaptation to ensure it's not used. + trackSelector.setParameters( + defaultParameters.buildUpon().setAllowAudioNonSeamlessAdaptiveness(true)); + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {seamlessAudioCapabilities}, + trackGroups, + periodId, + TIMELINE); + assertThat(result.length).isEqualTo(1); + assertFixedSelection(result.selections[0], trackGroups.get(0), /* expectedTrack= */ 0); + } + + @Test + public void + selectTracks_withMultipleAudioTracksWithNonSeamlessAdaptiveness_selectsAdaptiveOnlyIfAllowed() + throws Exception { + FakeRendererCapabilities nonSeamlessAudioCapabilities = + new FakeRendererCapabilities( + C.TRACK_TYPE_AUDIO, + RendererCapabilities.create( + FORMAT_HANDLED, ADAPTIVE_NOT_SEAMLESS, TUNNELING_NOT_SUPPORTED)); + + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + TrackGroupArray trackGroups = + singleTrackGroup(formatBuilder.setId("0").build(), formatBuilder.setId("1").build()); + trackSelector.setParameters( + defaultParameters.buildUpon().setAllowAudioNonSeamlessAdaptiveness(true)); + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {nonSeamlessAudioCapabilities}, + trackGroups, + periodId, + TIMELINE); + assertThat(result.length).isEqualTo(1); + assertAdaptiveSelection( + result.selections[0], trackGroups.get(0), /* expectedTracks...= */ 0, 1); + + trackSelector.setParameters( + defaultParameters.buildUpon().setAllowAudioNonSeamlessAdaptiveness(false)); + result = + trackSelector.selectTracks( + new RendererCapabilities[] {nonSeamlessAudioCapabilities}, + trackGroups, + periodId, + TIMELINE); + assertThat(result.length).isEqualTo(1); + assertFixedSelection(result.selections[0], trackGroups.get(0), /* expectedTrack= */ 0); + } + + @Test + public void selectTracks_withMultipleAudioTracksWithSeamlessAdaptiveness_selectsAdaptive() + throws Exception { + FakeRendererCapabilities seamlessAudioCapabilities = + new FakeRendererCapabilities( + C.TRACK_TYPE_AUDIO, + RendererCapabilities.create( + FORMAT_HANDLED, ADAPTIVE_SEAMLESS, TUNNELING_NOT_SUPPORTED)); + + Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); + TrackGroupArray trackGroups = + singleTrackGroup(formatBuilder.setId("0").build(), formatBuilder.setId("1").build()); + // Explicitly disallow non-seamless adaptation to ensure it's not used. + trackSelector.setParameters( + defaultParameters.buildUpon().setAllowAudioNonSeamlessAdaptiveness(false)); + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {seamlessAudioCapabilities}, + trackGroups, + periodId, + TIMELINE); + assertThat(result.length).isEqualTo(1); + assertAdaptiveSelection( + result.selections[0], trackGroups.get(0), /* expectedTracks...= */ 0, 1); + } + @Test public void selectTracksWithMultipleAudioTracksOverrideReturnsAdaptiveTrackSelection() throws Exception {