From 0fd24c2fa35db1f50c7fa16aee6a9df601794472 Mon Sep 17 00:00:00 2001 From: christosts Date: Thu, 9 Jun 2022 17:27:12 +0000 Subject: [PATCH] DefaultTrackSelector: Constrain audio channel count The track selector will select multi-channel formats when those can be spatialized, otherwise the selector will prefer stereo/mono audio tracks. When the device supports audio spatialization (Android 12L+), the DefaultTrackSelector will monitor for changes in the platform Spatializer and trigger a new track selection upon a Spatializer change event. Devices with a `television` UI mode are excluded from audio channel count constraints. #minor-release PiperOrigin-RevId: 453957269 (cherry picked from commit e2f0fd76730fd4042e8b2226300e5173b0179dc1) --- RELEASENOTES.md | 20 + .../media3/exoplayer/ExoPlayerImpl.java | 2 + .../exoplayer/offline/DownloadHelper.java | 2 + .../trackselection/DefaultTrackSelector.java | 397 ++++++++++++++++-- .../trackselection/TrackSelector.java | 17 +- .../DefaultTrackSelectorTest.java | 125 +++++- 6 files changed, 514 insertions(+), 49 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 52d85073d36..aef2c6fd92a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -39,6 +39,24 @@ `DefaultTrackSelector.Parameters.buildUpon` to return `DefaultTrackSelector.Parameters.Builder` instead of the deprecated `DefaultTrackSelector.ParametersBuilder`. + * Add + `DefaultTrackSelector.Parameters.constrainAudioChannelCountToDeviceCapabilities`. + which is enabled by default. When enabled, the `DefaultTrackSelector` + will prefer audio tracks whose channel count does not exceed the device + output capabilities. On handheld devices, the `DefaultTrackSelector` + will prefer stereo/mono over multichannel audio formats, unless the + multichannel format can be + [Spatialized](https://developer.android.com/reference/android/media/Spatializer) + (Android 12L+) or is a Dolby surround sound format. In addition, on + devices that support audio spatialization, the `DefaultTrackSelector` + will monitor for changes in the + [Spatializer properties](https://developer.android.com/reference/android/media/Spatializer.OnSpatializerStateChangedListener) + and trigger a new track selection upon these. Devices with a + `television` + [UI mode](https://developer.android.com/guide/topics/resources/providing-resources#UiModeQualifier) + are excluded from these constraints and the format with the highest + channel count will be preferred. To enable this feature, the + `DefaultTrackSelector` instance must be constructed with a `Context`. * Video: * Rename `DummySurface` to `PlaceholderSurface`. * Add AV1 support to the `MediaCodecVideoRenderer.getCodecMaxInputSize`. @@ -171,6 +189,8 @@ `DEFAULT_TRACK_SELECTOR_PARAMETERS` constants. Use `getDefaultTrackSelectorParameters(Context)` instead when possible, and `DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT` otherwise. + * Remove constructor `DefaultTrackSelector(ExoTrackSelection.Factory)`. + Use `DefaultTrackSelector(Context, ExoTrackSelection.Factory)` instead. ### 1.0.0-alpha03 (2022-03-14) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index 9f8cd262e5b..389112484a0 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -380,6 +380,7 @@ public ExoPlayerImpl(ExoPlayer.Builder builder, @Nullable Player wrappingPlayer) deviceInfo = createDeviceInfo(streamVolumeManager); videoSize = VideoSize.UNKNOWN; + trackSelector.setAudioAttributes(audioAttributes); sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_SESSION_ID, audioSessionId); sendRendererMessage(TRACK_TYPE_VIDEO, MSG_SET_AUDIO_SESSION_ID, audioSessionId); sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_ATTRIBUTES, audioAttributes); @@ -1375,6 +1376,7 @@ public void setAudioAttributes(AudioAttributes newAudioAttributes, boolean handl } audioFocusManager.setAudioAttributes(handleAudioFocus ? newAudioAttributes : null); + trackSelector.setAudioAttributes(newAudioAttributes); boolean playWhenReady = getPlayWhenReady(); @AudioFocusManager.PlayerCommand int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState()); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java index 25f6c981149..2874601a705 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java @@ -110,6 +110,7 @@ public final class DownloadHelper { DefaultTrackSelector.Parameters.DEFAULT_WITHOUT_CONTEXT .buildUpon() .setForceHighestSupportedBitrate(true) + .setConstrainAudioChannelCountToDeviceCapabilities(false) .build(); /** Returns the default parameters used for track selection for downloading. */ @@ -117,6 +118,7 @@ public static DefaultTrackSelector.Parameters getDefaultTrackSelectorParameters( return DefaultTrackSelector.Parameters.getDefaults(context) .buildUpon() .setForceHighestSupportedBitrate(true) + .setConstrainAudioChannelCountToDeviceCapabilities(false) .build(); } 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 f680f58e19e..fbd9f6e57d8 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 @@ -15,19 +15,29 @@ */ package androidx.media3.exoplayer.trackselection; +import static androidx.media3.common.util.Assertions.checkStateNotNull; +import static androidx.media3.common.util.Util.castNonNull; import static java.lang.annotation.ElementType.TYPE_USE; import static java.util.Collections.max; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Point; +import android.media.AudioFormat; +import android.media.AudioManager; +import android.media.Spatializer; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.text.TextUtils; import android.util.Pair; import android.util.SparseArray; import android.util.SparseBooleanArray; +import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.media3.common.AudioAttributes; import androidx.media3.common.Bundleable; import androidx.media3.common.C; import androidx.media3.common.C.FormatSupport; @@ -40,6 +50,7 @@ import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.BundleableUtil; +import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.ExoPlaybackException; @@ -50,6 +61,7 @@ import androidx.media3.exoplayer.RendererConfiguration; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.TrackGroupArray; +import com.google.common.base.Predicate; import com.google.common.collect.ComparisonChain; import com.google.common.collect.ImmutableList; import com.google.common.collect.Ordering; @@ -65,7 +77,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; import org.checkerframework.checker.nullness.compatqual.NullableType; /** @@ -101,6 +112,12 @@ @UnstableApi public class DefaultTrackSelector extends MappingTrackSelector { + private static final String TAG = "DefaultTrackSelector"; + private static final String AUDIO_CHANNEL_COUNT_CONSTRAINTS_WARN_MESSAGE = + "Audio channel count constraints cannot be applied without reference to Context. Build the" + + " track selector instance with one of the non-deprecated constructors that take a" + + " Context argument."; + /** * @deprecated Use {@link Parameters.Builder} instead. */ @@ -680,6 +697,7 @@ public static final class Builder extends TrackSelectionParameters.Builder { private boolean allowAudioMixedSampleRateAdaptiveness; private boolean allowAudioMixedChannelCountAdaptiveness; private boolean allowAudioMixedDecoderSupportAdaptiveness; + private boolean constrainAudioChannelCountToDeviceCapabilities; // General private boolean exceedRendererCapabilitiesIfNecessary; private boolean tunnelingEnabled; @@ -734,6 +752,8 @@ private Builder(Parameters initialValues) { initialValues.allowAudioMixedChannelCountAdaptiveness; allowAudioMixedDecoderSupportAdaptiveness = initialValues.allowAudioMixedDecoderSupportAdaptiveness; + constrainAudioChannelCountToDeviceCapabilities = + initialValues.constrainAudioChannelCountToDeviceCapabilities; // General exceedRendererCapabilitiesIfNecessary = initialValues.exceedRendererCapabilitiesIfNecessary; tunnelingEnabled = initialValues.tunnelingEnabled; @@ -746,6 +766,7 @@ private Builder(Parameters initialValues) { @SuppressWarnings("method.invocation") // Only setter are invoked. private Builder(Bundle bundle) { super(bundle); + init(); Parameters defaultValue = Parameters.DEFAULT_WITHOUT_CONTEXT; // Video setExceedVideoConstraintsIfNecessary( @@ -788,6 +809,11 @@ private Builder(Bundle bundle) { Parameters.keyForField( Parameters.FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS), defaultValue.allowAudioMixedDecoderSupportAdaptiveness)); + setConstrainAudioChannelCountToDeviceCapabilities( + bundle.getBoolean( + Parameters.keyForField( + Parameters.FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES), + defaultValue.constrainAudioChannelCountToDeviceCapabilities)); // General setExceedRendererCapabilitiesIfNecessary( bundle.getBoolean( @@ -1082,6 +1108,36 @@ public Builder setPreferredAudioMimeTypes(String... mimeTypes) { 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}. + * + *

When enabled, the track selector will prefer stereo/mono audio tracks over multichannel + * if the audio cannot be spatialized or the device is outputting stereo audio. For example, + * on a mobile device that outputs non-spatialized audio to its speakers. Dolby surround sound + * formats are excluded from these constraints because some Dolby decoders are known to + * spatialize multichannel audio on Android OS versions that don't support the {@link + * Spatializer} API. + * + *

For devices with Android 12L+ that support {@linkplain Spatializer audio + * spatialization}, when this is enabled the track selector will trigger a new track selection + * everytime a change in {@linkplain Spatializer.OnSpatializerStateChangedListener + * spatialization properties} is detected. + * + *

The constraints do not apply on devices with {@code + * television} UI mode. + * + *

The constraints do not apply when the track selector is created without a reference to a + * {@link Context} via the deprecated {@link + * DefaultTrackSelector#DefaultTrackSelector(TrackSelectionParameters, + * ExoTrackSelection.Factory)} constructor. + */ + public Builder setConstrainAudioChannelCountToDeviceCapabilities(boolean enabled) { + constrainAudioChannelCountToDeviceCapabilities = enabled; + return this; + } + // Text @Override @@ -1381,6 +1437,7 @@ private void init(Builder this) { allowAudioMixedSampleRateAdaptiveness = false; allowAudioMixedChannelCountAdaptiveness = false; allowAudioMixedDecoderSupportAdaptiveness = false; + constrainAudioChannelCountToDeviceCapabilities = true; // General exceedRendererCapabilitiesIfNecessary = true; tunnelingEnabled = false; @@ -1475,6 +1532,7 @@ public static Parameters getDefaults(Context context) { } // Video + /** * Whether to exceed the {@link #maxVideoWidth}, {@link #maxVideoHeight} and {@link * #maxVideoBitrate} constraints when no selection can be made otherwise. The default value is @@ -1499,6 +1557,9 @@ public static Parameters getDefaults(Context context) { * RendererCapabilities.HardwareAccelerationSupport}. */ public final boolean allowVideoMixedDecoderSupportAdaptiveness; + + // Audio + /** * Whether to exceed the {@link #maxAudioChannelCount} and {@link #maxAudioBitrate} constraints * when no selection can be made otherwise. The default value is {@code true}. @@ -1526,6 +1587,14 @@ public static Parameters getDefaults(Context context) { * RendererCapabilities.HardwareAccelerationSupport}. */ public final boolean allowAudioMixedDecoderSupportAdaptiveness; + /** + * 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}. + */ + public final boolean constrainAudioChannelCountToDeviceCapabilities; + + // General + /** * Whether to exceed renderer capabilities when no selection can be made otherwise. * @@ -1566,6 +1635,8 @@ private Parameters(Builder builder) { allowAudioMixedSampleRateAdaptiveness = builder.allowAudioMixedSampleRateAdaptiveness; allowAudioMixedChannelCountAdaptiveness = builder.allowAudioMixedChannelCountAdaptiveness; allowAudioMixedDecoderSupportAdaptiveness = builder.allowAudioMixedDecoderSupportAdaptiveness; + constrainAudioChannelCountToDeviceCapabilities = + builder.constrainAudioChannelCountToDeviceCapabilities; // General exceedRendererCapabilitiesIfNecessary = builder.exceedRendererCapabilitiesIfNecessary; tunnelingEnabled = builder.tunnelingEnabled; @@ -1654,6 +1725,8 @@ public boolean equals(@Nullable Object obj) { == other.allowAudioMixedChannelCountAdaptiveness && allowAudioMixedDecoderSupportAdaptiveness == other.allowAudioMixedDecoderSupportAdaptiveness + && constrainAudioChannelCountToDeviceCapabilities + == other.constrainAudioChannelCountToDeviceCapabilities // General && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary && tunnelingEnabled == other.tunnelingEnabled @@ -1678,6 +1751,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 + (constrainAudioChannelCountToDeviceCapabilities ? 1 : 0); // General result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0); result = 31 * result + (tunnelingEnabled ? 1 : 0); @@ -1712,6 +1786,8 @@ public int hashCode() { FIELD_CUSTOM_ID_BASE + 14; private static final int FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = FIELD_CUSTOM_ID_BASE + 15; + private static final int FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES = + FIELD_CUSTOM_ID_BASE + 16; @Override public Bundle toBundle() { @@ -1746,6 +1822,9 @@ public Bundle toBundle() { bundle.putBoolean( keyForField(FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS), allowAudioMixedDecoderSupportAdaptiveness); + bundle.putBoolean( + keyForField(FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES), + constrainAudioChannelCountToDeviceCapabilities); // General bundle.putBoolean( keyForField(FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY), @@ -2004,8 +2083,20 @@ private static String keyForField(@FieldNumber int field) { /** Ordering where all elements are equal. */ private static final Ordering NO_ORDER = Ordering.from((first, second) -> 0); + private final Object lock; + @Nullable public final Context context; private final ExoTrackSelection.Factory trackSelectionFactory; - private final AtomicReference parametersReference; + private final boolean deviceIsTV; + + @GuardedBy("lock") + private Parameters parameters; + + @GuardedBy("lock") + @Nullable + private SpatializerWrapperV32 spatializer; + + @GuardedBy("lock") + private AudioAttributes audioAttributes; /** * @deprecated Use {@link #DefaultTrackSelector(Context)} instead. @@ -2015,14 +2106,6 @@ public DefaultTrackSelector() { this(Parameters.DEFAULT_WITHOUT_CONTEXT, new AdaptiveTrackSelection.Factory()); } - /** - * @deprecated Use {@link #DefaultTrackSelector(Context, ExoTrackSelection.Factory)}. - */ - @Deprecated - public DefaultTrackSelector(ExoTrackSelection.Factory trackSelectionFactory) { - this(Parameters.DEFAULT_WITHOUT_CONTEXT, trackSelectionFactory); - } - /** * @param context Any {@link Context}. */ @@ -2035,26 +2118,88 @@ public DefaultTrackSelector(Context context) { * @param trackSelectionFactory A factory for {@link ExoTrackSelection}s. */ public DefaultTrackSelector(Context context, ExoTrackSelection.Factory trackSelectionFactory) { - this(Parameters.getDefaults(context), trackSelectionFactory); + this(context, Parameters.getDefaults(context), trackSelectionFactory); } /** + * @param context Any {@link Context}. * @param parameters Initial {@link TrackSelectionParameters}. - * @param trackSelectionFactory A factory for {@link ExoTrackSelection}s. */ + public DefaultTrackSelector(Context context, TrackSelectionParameters parameters) { + this(context, parameters, new AdaptiveTrackSelection.Factory()); + } + + /** + * @deprecated Use {@link #DefaultTrackSelector(Context, TrackSelectionParameters, + * ExoTrackSelection.Factory)} + */ + @Deprecated public DefaultTrackSelector( TrackSelectionParameters parameters, ExoTrackSelection.Factory trackSelectionFactory) { + this(parameters, trackSelectionFactory, /* context= */ null); + } + + /** + * @param context Any {@link Context}. + * @param parameters Initial {@link TrackSelectionParameters}. + * @param trackSelectionFactory A factory for {@link ExoTrackSelection}s. + */ + public DefaultTrackSelector( + Context context, + TrackSelectionParameters parameters, + ExoTrackSelection.Factory trackSelectionFactory) { + this(parameters, trackSelectionFactory, context); + } + + /** + * Exists for backwards compatibility so that the deprecated constructor {@link + * #DefaultTrackSelector(TrackSelectionParameters, ExoTrackSelection.Factory)} can initialize + * {@code context} with {@code null} while we don't have a public constructor with a {@code + * Nullable context}. + * + * @param context Any {@link Context}. + * @param parameters Initial {@link TrackSelectionParameters}. + * @param trackSelectionFactory A factory for {@link ExoTrackSelection}s. + */ + private DefaultTrackSelector( + TrackSelectionParameters parameters, + ExoTrackSelection.Factory trackSelectionFactory, + @Nullable Context context) { + this.lock = new Object(); + this.context = context != null ? context.getApplicationContext() : null; this.trackSelectionFactory = trackSelectionFactory; - parametersReference = - new AtomicReference<>( - parameters instanceof Parameters - ? (Parameters) parameters - : Parameters.DEFAULT_WITHOUT_CONTEXT.buildUpon().set(parameters).build()); + if (parameters instanceof Parameters) { + this.parameters = (Parameters) parameters; + } else { + Parameters defaultParameters = + context == null ? Parameters.DEFAULT_WITHOUT_CONTEXT : Parameters.getDefaults(context); + this.parameters = defaultParameters.buildUpon().set(parameters).build(); + } + this.audioAttributes = AudioAttributes.DEFAULT; + this.deviceIsTV = context != null && Util.isTv(context); + if (!deviceIsTV && context != null && Util.SDK_INT >= 32) { + spatializer = SpatializerWrapperV32.tryCreateInstance(context); + } + if (this.parameters.constrainAudioChannelCountToDeviceCapabilities && context == null) { + Log.w(TAG, AUDIO_CHANNEL_COUNT_CONSTRAINTS_WARN_MESSAGE); + } + } + + @Override + public void release() { + synchronized (lock) { + if (Util.SDK_INT >= 32 && spatializer != null) { + spatializer.release(); + } + } + super.release(); } @Override public Parameters getParameters() { - return parametersReference.get(); + synchronized (lock) { + return parameters; + } } @Override @@ -2068,11 +2213,22 @@ public void setParameters(TrackSelectionParameters parameters) { setParametersInternal((Parameters) parameters); } // Only add the fields of `TrackSelectionParameters` to `parameters`. - Parameters mergedParameters = - new Parameters.Builder(parametersReference.get()).set(parameters).build(); + Parameters mergedParameters = new Parameters.Builder(getParameters()).set(parameters).build(); setParametersInternal(mergedParameters); } + @Override + public void setAudioAttributes(AudioAttributes audioAttributes) { + boolean audioAttributesChanged; + synchronized (lock) { + audioAttributesChanged = !this.audioAttributes.equals(audioAttributes); + this.audioAttributes = audioAttributes; + } + if (audioAttributesChanged) { + maybeInvalidateForAudioChannelCountConstraints(); + } + } + /** * @deprecated Use {@link #setParameters(Parameters.Builder)} instead. */ @@ -2103,7 +2259,16 @@ public Parameters.Builder buildUponParameters() { */ private void setParametersInternal(Parameters parameters) { Assertions.checkNotNull(parameters); - if (!parametersReference.getAndSet(parameters).equals(parameters)) { + boolean parametersChanged; + synchronized (lock) { + parametersChanged = !this.parameters.equals(parameters); + this.parameters = parameters; + } + + if (parametersChanged) { + if (parameters.constrainAudioChannelCountToDeviceCapabilities && context == null) { + Log.w(TAG, AUDIO_CHANNEL_COUNT_CONSTRAINTS_WARN_MESSAGE); + } invalidate(); } } @@ -2119,22 +2284,33 @@ private void setParametersInternal(Parameters parameters) { MediaPeriodId mediaPeriodId, Timeline timeline) throws ExoPlaybackException { - Parameters params = parametersReference.get(); + Parameters parameters; + synchronized (lock) { + parameters = this.parameters; + if (parameters.constrainAudioChannelCountToDeviceCapabilities + && Util.SDK_INT >= 32 + && spatializer != null) { + // Initialize the spatializer now so we can get a reference to the playback looper with + // Looper.myLooper(). + spatializer.ensureInitialized(this, checkStateNotNull(Looper.myLooper())); + } + } int rendererCount = mappedTrackInfo.getRendererCount(); ExoTrackSelection.@NullableType Definition[] definitions = selectAllTracks( mappedTrackInfo, rendererFormatSupports, rendererMixedMimeTypeAdaptationSupports, - params); + parameters); - applyTrackSelectionOverrides(mappedTrackInfo, params, definitions); - applyLegacyRendererOverrides(mappedTrackInfo, params, definitions); + applyTrackSelectionOverrides(mappedTrackInfo, parameters, definitions); + applyLegacyRendererOverrides(mappedTrackInfo, parameters, definitions); // Disable renderers if needed. for (int i = 0; i < rendererCount; i++) { @C.TrackType int rendererType = mappedTrackInfo.getRendererType(i); - if (params.getRendererDisabled(i) || params.disabledTrackTypes.contains(rendererType)) { + if (parameters.getRendererDisabled(i) + || parameters.disabledTrackTypes.contains(rendererType)) { definitions[i] = null; } } @@ -2151,7 +2327,7 @@ private void setParametersInternal(Parameters parameters) { for (int i = 0; i < rendererCount; i++) { @C.TrackType int rendererType = mappedTrackInfo.getRendererType(i); boolean forceRendererDisabled = - params.getRendererDisabled(i) || params.disabledTrackTypes.contains(rendererType); + parameters.getRendererDisabled(i) || parameters.disabledTrackTypes.contains(rendererType); boolean rendererEnabled = !forceRendererDisabled && (mappedTrackInfo.getRendererType(i) == C.TRACK_TYPE_NONE @@ -2160,7 +2336,7 @@ private void setParametersInternal(Parameters parameters) { } // Configure audio and video renderers to use tunneling if appropriate. - if (params.tunnelingEnabled) { + if (parameters.tunnelingEnabled) { maybeConfigureRenderersForTunneling( mappedTrackInfo, rendererFormatSupports, rendererConfigurations, rendererTrackSelections); } @@ -2316,10 +2492,50 @@ protected Pair selectAudioTrack( rendererFormatSupports, (int rendererIndex, TrackGroup group, @Capabilities int[] support) -> AudioTrackInfo.createForTrackGroup( - rendererIndex, group, params, support, hasVideoRendererWithMappedTracksFinal), + rendererIndex, + group, + params, + support, + hasVideoRendererWithMappedTracksFinal, + this::isAudioFormatWithinAudioChannelCountConstraints), AudioTrackInfo::compareSelections); } + /** + * Returns whether an audio format is within the audio channel count constraints. + * + *

This method returns {@code true} if one of the following holds: + * + *

+ */ + private boolean isAudioFormatWithinAudioChannelCountConstraints(Format format) { + synchronized (lock) { + return !parameters.constrainAudioChannelCountToDeviceCapabilities + || deviceIsTV + || format.channelCount <= 2 + || (isDolbyAudio(format) + && (Util.SDK_INT < 32 + || spatializer == null + || !spatializer.isSpatializationSupported())) + || (Util.SDK_INT >= 32 + && spatializer != null + && spatializer.isSpatializationSupported() + && spatializer.isAvailable() + && spatializer.isEnabled() + && spatializer.canBeSpatialized(audioAttributes, format)); + } + } + // Text track selection implementation. /** @@ -2453,6 +2669,21 @@ private > Pair sel firstTrackInfo.rendererIndex); } + private void maybeInvalidateForAudioChannelCountConstraints() { + boolean shouldInvalidate; + synchronized (lock) { + shouldInvalidate = + parameters.constrainAudioChannelCountToDeviceCapabilities + && !deviceIsTV + && Util.SDK_INT >= 32 + && spatializer != null + && spatializer.isSpatializationSupported(); + } + if (shouldInvalidate) { + invalidate(); + } + } + // Utility methods. private static void applyTrackSelectionOverrides( @@ -2777,6 +3008,21 @@ private static int getVideoCodecPreferenceScore(@Nullable String mimeType) { } } + private static boolean isDolbyAudio(Format format) { + if (format.sampleMimeType == null) { + return false; + } + switch (format.sampleMimeType) { + case MimeTypes.AUDIO_AC3: + case MimeTypes.AUDIO_E_AC3: + case MimeTypes.AUDIO_E_AC3_JOC: + case MimeTypes.AUDIO_AC4: + return true; + default: + return false; + } + } + /** Base class for track selection information of a {@link Format}. */ private abstract static class TrackInfo> { /** Factory for {@link TrackInfo} implementations for a given {@link TrackGroup}. */ @@ -3026,7 +3272,8 @@ public static ImmutableList createForTrackGroup( TrackGroup trackGroup, Parameters params, @Capabilities int[] formatSupport, - boolean hasMappedVideoTracks) { + boolean hasMappedVideoTracks, + Predicate withinAudioChannelCountConstraints) { ImmutableList.Builder listBuilder = ImmutableList.builder(); for (int i = 0; i < trackGroup.length; i++) { listBuilder.add( @@ -3036,7 +3283,8 @@ public static ImmutableList createForTrackGroup( /* trackIndex= */ i, params, formatSupport[i], - hasMappedVideoTracks)); + hasMappedVideoTracks, + withinAudioChannelCountConstraints)); } return listBuilder.build(); } @@ -3066,7 +3314,8 @@ public AudioTrackInfo( int trackIndex, Parameters parameters, @Capabilities int formatSupport, - boolean hasMappedVideoTracks) { + boolean hasMappedVideoTracks, + Predicate withinAudioChannelCountConstraints) { super(rendererIndex, trackGroup, trackIndex); this.parameters = parameters; this.language = normalizeUndeterminedLanguageToNull(format.language); @@ -3098,7 +3347,8 @@ public AudioTrackInfo( isWithinConstraints = (format.bitrate == Format.NO_VALUE || format.bitrate <= parameters.maxAudioBitrate) && (format.channelCount == Format.NO_VALUE - || format.channelCount <= parameters.maxAudioChannelCount); + || format.channelCount <= parameters.maxAudioChannelCount) + && withinAudioChannelCountConstraints.apply(format); String[] localeLanguages = Util.getSystemLanguageCodes(); int bestLocaleMatchIndex = Integer.MAX_VALUE; int bestLocaleMatchScore = 0; @@ -3375,4 +3625,85 @@ public int compareTo(OtherTrackScore other) { .result(); } } + + /** + * Wraps the {@link Spatializer} in order to encapsulate its APIs within an inner class, to avoid + * runtime linking on devices with {@code API < 32}. + */ + @RequiresApi(32) + private static class SpatializerWrapperV32 { + + private final Spatializer spatializer; + private final boolean spatializationSupported; + + @Nullable private Handler handler; + @Nullable private Spatializer.OnSpatializerStateChangedListener listener; + + @Nullable + public static SpatializerWrapperV32 tryCreateInstance(Context context) { + @Nullable + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + return audioManager == null ? null : new SpatializerWrapperV32(audioManager.getSpatializer()); + } + + private SpatializerWrapperV32(Spatializer spatializer) { + this.spatializer = spatializer; + this.spatializationSupported = + spatializer.getImmersiveAudioLevel() != Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; + } + + public void ensureInitialized(DefaultTrackSelector defaultTrackSelector, Looper looper) { + if (listener != null || handler != null) { + return; + } + this.listener = + new Spatializer.OnSpatializerStateChangedListener() { + @Override + public void onSpatializerEnabledChanged(Spatializer spatializer, boolean enabled) { + defaultTrackSelector.maybeInvalidateForAudioChannelCountConstraints(); + } + + @Override + public void onSpatializerAvailableChanged(Spatializer spatializer, boolean available) { + defaultTrackSelector.maybeInvalidateForAudioChannelCountConstraints(); + } + }; + this.handler = new Handler(looper); + spatializer.addOnSpatializerStateChangedListener(handler::post, listener); + } + + public boolean isSpatializationSupported() { + return spatializationSupported; + } + + public boolean isAvailable() { + return spatializer.isAvailable(); + } + + public boolean isEnabled() { + return spatializer.isEnabled(); + } + + public boolean canBeSpatialized(AudioAttributes audioAttributes, Format format) { + AudioFormat.Builder builder = + new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setChannelMask(Util.getAudioTrackChannelConfig(format.channelCount)); + if (format.sampleRate != Format.NO_VALUE) { + builder.setSampleRate(format.sampleRate); + } + return spatializer.canBeSpatialized( + audioAttributes.getAudioAttributesV21().audioAttributes, builder.build()); + } + + public void release() { + if (listener == null || handler == null) { + return; + } + spatializer.removeOnSpatializerStateChangedListener(listener); + castNonNull(handler).removeCallbacksAndMessages(/* token= */ null); + handler = null; + listener = null; + } + } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelector.java index bfde8b19c5a..f6ca0f3eee4 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/TrackSelector.java @@ -17,7 +17,9 @@ import static androidx.media3.common.util.Assertions.checkStateNotNull; +import androidx.annotation.CallSuper; import androidx.annotation.Nullable; +import androidx.media3.common.AudioAttributes; import androidx.media3.common.Player; import androidx.media3.common.Timeline; import androidx.media3.common.TrackSelectionParameters; @@ -112,7 +114,8 @@ public interface InvalidationListener { * it has previously made are no longer valid. * @param bandwidthMeter A bandwidth meter which can be used by track selections to select tracks. */ - public final void init(InvalidationListener listener, BandwidthMeter bandwidthMeter) { + @CallSuper + public void init(InvalidationListener listener, BandwidthMeter bandwidthMeter) { this.listener = listener; this.bandwidthMeter = bandwidthMeter; } @@ -121,9 +124,10 @@ public final void init(InvalidationListener listener, BandwidthMeter bandwidthMe * Called by the player to release the selector. The selector cannot be used until {@link * #init(InvalidationListener, BandwidthMeter)} is called again. */ - public final void release() { - this.listener = null; - this.bandwidthMeter = null; + @CallSuper + public void release() { + listener = null; + bandwidthMeter = null; } /** @@ -178,6 +182,11 @@ public boolean isSetParametersSupported() { return false; } + /** Called by the player to set the {@link AudioAttributes} that will be used for playback. */ + public void setAudioAttributes(AudioAttributes audioAttributes) { + // Default implementation is no-op. + } + /** * Calls {@link InvalidationListener#onTrackSelectionsInvalidated()} to invalidate all previously * generated track selections. 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 60d69cdb993..a90b542769d 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 @@ -31,9 +31,9 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; import android.content.Context; +import android.media.Spatializer; import androidx.media3.common.Bundleable; import androidx.media3.common.C; import androidx.media3.common.Format; @@ -68,14 +68,19 @@ import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; /** Unit tests for {@link DefaultTrackSelector}. */ @RunWith(AndroidJUnit4.class) public final class DefaultTrackSelectorTest { + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + private static final RendererCapabilities ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO); private static final RendererCapabilities ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = @@ -142,7 +147,6 @@ public static void setUpBeforeClass() { @Before public void setUp() { - initMocks(this); when(bandwidthMeter.getBitrateEstimate()).thenReturn(1000000L); Context context = ApplicationProvider.getApplicationContext(); defaultParameters = Parameters.getDefaults(context); @@ -877,11 +881,18 @@ public void selectTracksPreferTrackWithinCapabilitiesOverSelectionFlagAndPreferr * are the same, and tracks are within renderer's capabilities. */ @Test - public void selectTracksWithinCapabilitiesSelectHigherNumChannel() throws Exception { + public void + selectTracks_audioChannelCountConstraintsDisabledAndTracksWithinCapabilities_selectHigherNumChannel() + throws Exception { Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); Format higherChannelFormat = formatBuilder.setChannelCount(6).build(); Format lowerChannelFormat = formatBuilder.setChannelCount(2).build(); TrackGroupArray trackGroups = wrapFormats(higherChannelFormat, lowerChannelFormat); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setConstrainAudioChannelCountToDeviceCapabilities(false) + .build()); TrackSelectorResult result = trackSelector.selectTracks( @@ -957,11 +968,13 @@ public void selectAudioTracks_withinCapabilities_andDifferentLanguage_selectsFir /** * Tests that track selector will prefer audio tracks with higher channel count over tracks with - * higher sample rate when other factors are the same, and tracks are within renderer's - * capabilities. + * higher sample rate when audio channel count constraints are disabled, other factors are the + * same, and tracks are within renderer's capabilities. */ @Test - public void selectTracksPreferHigherNumChannelBeforeSampleRate() throws Exception { + public void + selectTracks_audioChannelCountConstraintsDisabled_preferHigherNumChannelBeforeSampleRate() + throws Exception { Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); Format higherChannelLowerSampleRateFormat = formatBuilder.setChannelCount(6).setSampleRate(22050).build(); @@ -969,6 +982,11 @@ public void selectTracksPreferHigherNumChannelBeforeSampleRate() throws Exceptio formatBuilder.setChannelCount(2).setSampleRate(44100).build(); TrackGroupArray trackGroups = wrapFormats(higherChannelLowerSampleRateFormat, lowerChannelHigherSampleRateFormat); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setConstrainAudioChannelCountToDeviceCapabilities(false) + .build()); TrackSelectorResult result = trackSelector.selectTracks( @@ -1454,9 +1472,67 @@ public void selectTracksWithMultipleAudioTracks() throws Exception { assertAdaptiveSelection(result.selections[0], trackGroups.get(0), 0, 1); } + /** + * The following test is subject to the execution context. It currently runs on SDK 30 and the + * environment matches a handheld device ({@link Util#isTv(Context)} returns {@code false}) and + * there is no {@link android.media.Spatializer}. If the execution environment upgrades, the test + * may start failing depending on how the Robolectric Spatializer behaves. If the test starts + * failing, and Robolectric offers a shadow Spatializer whose behavior can be controlled, revise + * this test so that the Spatializer cannot spatialize the multichannel format. Also add tests + * where the Spatializer can spatialize multichannel formats and the track selector picks a + * multichannel format. + */ @Test - public void selectTracks_multipleAudioTracks_selectsAllTracksInBestConfigurationOnly() - throws Exception { + public void selectTracks_stereoAndMultichannelAACTracks_selectsStereo() + throws ExoPlaybackException { + Format stereoAudioFormat = AUDIO_FORMAT.buildUpon().setChannelCount(2).setId("0").build(); + Format multichannelAudioFormat = AUDIO_FORMAT.buildUpon().setChannelCount(6).setId("1").build(); + TrackGroupArray trackGroups = singleTrackGroup(stereoAudioFormat, multichannelAudioFormat); + + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); + + assertThat(result.length).isEqualTo(1); + assertThat(result.selections[0].getSelectedFormat()).isSameInstanceAs(stereoAudioFormat); + } + + /** + * The following test is subject to the execution context. It currently runs on SDK 30 and the + * environment matches a handheld device ({@link Util#isTv(Context)} returns {@code false}) and + * there is no {@link android.media.Spatializer}. If the execution environment upgrades, the test + * may start failing depending on how the Robolectric Spatializer behaves. If the test starts + * failing, and Robolectric offers a shadow Spatializer whose behavior can be controlled, revise + * this test so that the Spatializer does not support spatialization ({@link + * Spatializer#getImmersiveAudioLevel()} returns {@link + * Spatializer#SPATIALIZER_IMMERSIVE_LEVEL_NONE}). + */ + @Test + public void + selectTracks_withAACStereoAndDolbyMultichannelTrackWithinCapabilities_selectsDolbyMultichannelTrack() + throws ExoPlaybackException { + Format stereoAudioFormat = AUDIO_FORMAT.buildUpon().setChannelCount(2).setId("0").build(); + Format multichannelAudioFormat = + AUDIO_FORMAT + .buildUpon() + .setSampleMimeType(MimeTypes.AUDIO_AC3) + .setChannelCount(6) + .setId("1") + .build(); + TrackGroupArray trackGroups = singleTrackGroup(stereoAudioFormat, multichannelAudioFormat); + + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); + + assertThat(result.length).isEqualTo(1); + assertThat(result.selections[0].getSelectedFormat()).isSameInstanceAs(multichannelAudioFormat); + } + + @Test + public void + selectTracks_audioChannelCountConstraintsDisabledAndMultipleAudioTracks_selectsAllTracksInBestConfigurationOnly() + throws Exception { TrackGroupArray trackGroups = singleTrackGroup( buildAudioFormatWithConfiguration( @@ -1476,6 +1552,10 @@ public void selectTracks_multipleAudioTracks_selectsAllTracksInBestConfiguration /* channelCount= */ 6, MimeTypes.AUDIO_AAC, /* sampleRate= */ 44100)); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setConstrainAudioChannelCountToDeviceCapabilities(false)); TrackSelectorResult result = trackSelector.selectTracks( @@ -1568,10 +1648,17 @@ public void selectTracksWithMultipleAudioTracksWithMixedMimeTypes() throws Excep } @Test - public void selectTracksWithMultipleAudioTracksWithMixedChannelCounts() throws Exception { + public void + selectTracks_audioChannelCountConstraintsDisabledAndMultipleAudioTracksWithMixedChannelCounts() + throws Exception { Format.Builder formatBuilder = AUDIO_FORMAT.buildUpon(); Format stereoAudioFormat = formatBuilder.setChannelCount(2).build(); Format surroundAudioFormat = formatBuilder.setChannelCount(5).build(); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setConstrainAudioChannelCountToDeviceCapabilities(false) + .build()); // Should not adapt between different channel counts, so we expect a fixed selection containing // the track with more channels. @@ -1592,7 +1679,11 @@ public void selectTracksWithMultipleAudioTracksWithMixedChannelCounts() throws E // If we constrain the channel count to 4 we expect a fixed selection containing the track with // fewer channels. - trackSelector.setParameters(defaultParameters.buildUpon().setMaxAudioChannelCount(4)); + trackSelector.setParameters( + defaultParameters + .buildUpon() + .setConstrainAudioChannelCountToDeviceCapabilities(false) + .setMaxAudioChannelCount(4)); result = trackSelector.selectTracks( new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1601,7 +1692,11 @@ public void selectTracksWithMultipleAudioTracksWithMixedChannelCounts() throws E // If we constrain the channel count to 2 we expect a fixed selection containing the track with // fewer channels. - trackSelector.setParameters(defaultParameters.buildUpon().setMaxAudioChannelCount(2)); + trackSelector.setParameters( + defaultParameters + .buildUpon() + .setConstrainAudioChannelCountToDeviceCapabilities(false) + .setMaxAudioChannelCount(2)); result = trackSelector.selectTracks( new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1610,7 +1705,11 @@ public void selectTracksWithMultipleAudioTracksWithMixedChannelCounts() throws E // If we constrain the channel count to 1 we expect a fixed selection containing the track with // fewer channels. - trackSelector.setParameters(defaultParameters.buildUpon().setMaxAudioChannelCount(1)); + trackSelector.setParameters( + defaultParameters + .buildUpon() + .setConstrainAudioChannelCountToDeviceCapabilities(false) + .setMaxAudioChannelCount(1)); result = trackSelector.selectTracks( new RendererCapabilities[] {AUDIO_CAPABILITIES}, trackGroups, periodId, TIMELINE); @@ -1621,6 +1720,7 @@ public void selectTracksWithMultipleAudioTracksWithMixedChannelCounts() throws E trackSelector.setParameters( defaultParameters .buildUpon() + .setConstrainAudioChannelCountToDeviceCapabilities(false) .setMaxAudioChannelCount(1) .setExceedAudioConstraintsIfNecessary(false)); result = @@ -2399,6 +2499,7 @@ private static Parameters buildParametersForEqualsTest() { .setAllowAudioMixedChannelCountAdaptiveness(true) .setAllowAudioMixedDecoderSupportAdaptiveness(false) .setPreferredAudioMimeTypes(MimeTypes.AUDIO_AC3, MimeTypes.AUDIO_E_AC3) + .setConstrainAudioChannelCountToDeviceCapabilities(false) // Text .setPreferredTextLanguages("de", "en") .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION)