diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 52d85073d3..aef2c6fd92 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 9f8cd262e5..389112484a 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 @@ import java.util.concurrent.TimeoutException;
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 @@ import java.util.concurrent.TimeoutException;
}
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 25f6c98114..2874601a70 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 final class DownloadHelper {
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 f680f58e19..fbd9f6e57d 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.TrackSelectionOverride;
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.RendererCapabilities.Capabilities;
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.HashMap;
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 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
@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 class DefaultTrackSelector extends MappingTrackSelector {
private boolean allowAudioMixedSampleRateAdaptiveness;
private boolean allowAudioMixedChannelCountAdaptiveness;
private boolean allowAudioMixedDecoderSupportAdaptiveness;
+ private boolean constrainAudioChannelCountToDeviceCapabilities;
// General
private boolean exceedRendererCapabilitiesIfNecessary;
private boolean tunnelingEnabled;
@@ -734,6 +752,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
initialValues.allowAudioMixedChannelCountAdaptiveness;
allowAudioMixedDecoderSupportAdaptiveness =
initialValues.allowAudioMixedDecoderSupportAdaptiveness;
+ constrainAudioChannelCountToDeviceCapabilities =
+ initialValues.constrainAudioChannelCountToDeviceCapabilities;
// General
exceedRendererCapabilitiesIfNecessary = initialValues.exceedRendererCapabilitiesIfNecessary;
tunnelingEnabled = initialValues.tunnelingEnabled;
@@ -746,6 +766,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
@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 @@ public class DefaultTrackSelector extends MappingTrackSelector {
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 class DefaultTrackSelector extends MappingTrackSelector {
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 @@ public class DefaultTrackSelector extends MappingTrackSelector {
allowAudioMixedSampleRateAdaptiveness = false;
allowAudioMixedChannelCountAdaptiveness = false;
allowAudioMixedDecoderSupportAdaptiveness = false;
+ constrainAudioChannelCountToDeviceCapabilities = true;
// General
exceedRendererCapabilitiesIfNecessary = true;
tunnelingEnabled = false;
@@ -1475,6 +1532,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
// 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 class DefaultTrackSelector extends MappingTrackSelector {
* 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 class DefaultTrackSelector extends MappingTrackSelector {
* 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 @@ public class DefaultTrackSelector extends MappingTrackSelector {
allowAudioMixedSampleRateAdaptiveness = builder.allowAudioMixedSampleRateAdaptiveness;
allowAudioMixedChannelCountAdaptiveness = builder.allowAudioMixedChannelCountAdaptiveness;
allowAudioMixedDecoderSupportAdaptiveness = builder.allowAudioMixedDecoderSupportAdaptiveness;
+ constrainAudioChannelCountToDeviceCapabilities =
+ builder.constrainAudioChannelCountToDeviceCapabilities;
// General
exceedRendererCapabilitiesIfNecessary = builder.exceedRendererCapabilitiesIfNecessary;
tunnelingEnabled = builder.tunnelingEnabled;
@@ -1654,6 +1725,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
== other.allowAudioMixedChannelCountAdaptiveness
&& allowAudioMixedDecoderSupportAdaptiveness
== other.allowAudioMixedDecoderSupportAdaptiveness
+ && constrainAudioChannelCountToDeviceCapabilities
+ == other.constrainAudioChannelCountToDeviceCapabilities
// General
&& exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary
&& tunnelingEnabled == other.tunnelingEnabled
@@ -1678,6 +1751,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
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 class DefaultTrackSelector extends MappingTrackSelector {
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 class DefaultTrackSelector extends MappingTrackSelector {
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 @@ public class DefaultTrackSelector extends MappingTrackSelector {
/** 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 class DefaultTrackSelector extends MappingTrackSelector {
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 class DefaultTrackSelector extends MappingTrackSelector {
* @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}.
+ */
+ 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(
- TrackSelectionParameters parameters, ExoTrackSelection.Factory trackSelectionFactory) {
+ 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 class DefaultTrackSelector extends MappingTrackSelector {
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 class DefaultTrackSelector extends MappingTrackSelector {
*/
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 @@ public class DefaultTrackSelector extends MappingTrackSelector {
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 @@ public class DefaultTrackSelector extends MappingTrackSelector {
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 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
// 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 @@ public class DefaultTrackSelector extends MappingTrackSelector {
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:
+ *
+ *
+ * - Audio channel count constraints are not applicable (all formats are considered within
+ * constraints).
+ *
- The device has a {@code
+ * television} UI mode.
+ *
- {@code format} has up to 2 channels.
+ *
- The device does not support audio spatialization and the format is {@linkplain
+ * #isDolbyAudio(Format) a Dolby one}.
+ *
- Audio spatialization is applicable and {@code format} can be spatialized.
+ *
+ */
+ 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 @@ public class DefaultTrackSelector extends MappingTrackSelector {
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 @@ public class DefaultTrackSelector extends MappingTrackSelector {
}
}
+ 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 class DefaultTrackSelector extends MappingTrackSelector {
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 class DefaultTrackSelector extends MappingTrackSelector {
/* trackIndex= */ i,
params,
formatSupport[i],
- hasMappedVideoTracks));
+ hasMappedVideoTracks,
+ withinAudioChannelCountConstraints));
}
return listBuilder.build();
}
@@ -3066,7 +3314,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
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 class DefaultTrackSelector extends MappingTrackSelector {
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 class DefaultTrackSelector extends MappingTrackSelector {
.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 bfde8b19c5..f6ca0f3eee 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 @@ package androidx.media3.exoplayer.trackselection;
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 abstract class TrackSelector {
* 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 abstract class TrackSelector {
* 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 abstract class TrackSelector {
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 60d69cdb99..a90b542769 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.never;
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 java.util.Map;
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 final class DefaultTrackSelectorTest {
@Before
public void setUp() {
- initMocks(this);
when(bandwidthMeter.getBitrateEstimate()).thenReturn(1000000L);
Context context = ApplicationProvider.getApplicationContext();
defaultParameters = Parameters.getDefaults(context);
@@ -877,11 +881,18 @@ public final class DefaultTrackSelectorTest {
* 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 final class DefaultTrackSelectorTest {
/**
* 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 final class DefaultTrackSelectorTest {
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 final class DefaultTrackSelectorTest {
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 final class DefaultTrackSelectorTest {
/* channelCount= */ 6,
MimeTypes.AUDIO_AAC,
/* sampleRate= */ 44100));
+ trackSelector.setParameters(
+ trackSelector
+ .buildUponParameters()
+ .setConstrainAudioChannelCountToDeviceCapabilities(false));
TrackSelectorResult result =
trackSelector.selectTracks(
@@ -1568,10 +1648,17 @@ public final class DefaultTrackSelectorTest {
}
@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 final class DefaultTrackSelectorTest {
// 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 final class DefaultTrackSelectorTest {
// 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 final class DefaultTrackSelectorTest {
// 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 final class DefaultTrackSelectorTest {
trackSelector.setParameters(
defaultParameters
.buildUpon()
+ .setConstrainAudioChannelCountToDeviceCapabilities(false)
.setMaxAudioChannelCount(1)
.setExceedAudioConstraintsIfNecessary(false));
result =
@@ -2399,6 +2499,7 @@ public final class DefaultTrackSelectorTest {
.setAllowAudioMixedChannelCountAdaptiveness(true)
.setAllowAudioMixedDecoderSupportAdaptiveness(false)
.setPreferredAudioMimeTypes(MimeTypes.AUDIO_AC3, MimeTypes.AUDIO_E_AC3)
+ .setConstrainAudioChannelCountToDeviceCapabilities(false)
// Text
.setPreferredTextLanguages("de", "en")
.setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION)