From 9f3c595e022bc98fd776a16dcad10adc66b37e67 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 --- .../android/exoplayer2/ExoPlayerImpl.java | 2 + .../exoplayer2/offline/DownloadHelper.java | 2 + .../trackselection/DefaultTrackSelector.java | 397 ++++++++++++++++-- .../trackselection/TrackSelector.java | 17 +- .../DefaultTrackSelectorTest.java | 125 +++++- 5 files changed, 494 insertions(+), 49 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 726403e5a2..121c2cab1f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -369,6 +369,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); @@ -1364,6 +1365,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/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 9168b30350..6575c8fc22 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -107,6 +107,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. */ @@ -114,6 +115,7 @@ public final class DownloadHelper { return DefaultTrackSelector.Parameters.getDefaults(context) .buildUpon() .setForceHighestSupportedBitrate(true) + .setConstrainAudioChannelCountToDeviceCapabilities(false) .build(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index e85a0bf21e..92fb7544af 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -15,19 +15,28 @@ */ package com.google.android.exoplayer2.trackselection; +import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; +import static com.google.android.exoplayer2.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 com.google.android.exoplayer2.Bundleable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.FormatSupport; @@ -40,13 +49,16 @@ import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; import com.google.android.exoplayer2.RendererCapabilities.Capabilities; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.BundleableUtil; +import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; +import com.google.common.base.Predicate; import com.google.common.collect.ComparisonChain; import com.google.common.collect.ImmutableList; import com.google.common.collect.Ordering; @@ -62,7 +74,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; /** @@ -97,6 +108,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; */ 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. */ @@ -676,6 +693,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; @@ -730,6 +748,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { initialValues.allowAudioMixedChannelCountAdaptiveness; allowAudioMixedDecoderSupportAdaptiveness = initialValues.allowAudioMixedDecoderSupportAdaptiveness; + constrainAudioChannelCountToDeviceCapabilities = + initialValues.constrainAudioChannelCountToDeviceCapabilities; // General exceedRendererCapabilitiesIfNecessary = initialValues.exceedRendererCapabilitiesIfNecessary; tunnelingEnabled = initialValues.tunnelingEnabled; @@ -742,6 +762,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( @@ -784,6 +805,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( @@ -1078,6 +1104,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 @@ -1377,6 +1433,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { allowAudioMixedSampleRateAdaptiveness = false; allowAudioMixedChannelCountAdaptiveness = false; allowAudioMixedDecoderSupportAdaptiveness = false; + constrainAudioChannelCountToDeviceCapabilities = true; // General exceedRendererCapabilitiesIfNecessary = true; tunnelingEnabled = false; @@ -1471,6 +1528,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 @@ -1495,6 +1553,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}. @@ -1522,6 +1583,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. * @@ -1562,6 +1631,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; @@ -1650,6 +1721,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { == other.allowAudioMixedChannelCountAdaptiveness && allowAudioMixedDecoderSupportAdaptiveness == other.allowAudioMixedDecoderSupportAdaptiveness + && constrainAudioChannelCountToDeviceCapabilities + == other.constrainAudioChannelCountToDeviceCapabilities // General && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary && tunnelingEnabled == other.tunnelingEnabled @@ -1674,6 +1747,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); @@ -1708,6 +1782,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() { @@ -1742,6 +1818,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), @@ -1997,8 +2076,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. @@ -2008,14 +2099,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}. */ @@ -2028,26 +2111,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 @@ -2061,11 +2206,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. */ @@ -2096,7 +2252,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(); } } @@ -2112,22 +2277,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; } } @@ -2144,7 +2320,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 @@ -2153,7 +2329,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); } @@ -2309,10 +2485,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: + * + *

+ */ + 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. /** @@ -2446,6 +2662,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( @@ -2770,6 +3001,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}. */ @@ -3019,7 +3265,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( @@ -3029,7 +3276,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { /* trackIndex= */ i, params, formatSupport[i], - hasMappedVideoTracks)); + hasMappedVideoTracks, + withinAudioChannelCountConstraints)); } return listBuilder.build(); } @@ -3059,7 +3307,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); @@ -3091,7 +3340,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; @@ -3368,4 +3618,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/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java index c506d03e78..06f32480d7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.trackselection; import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; +import androidx.annotation.CallSuper; import androidx.annotation.Nullable; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; @@ -25,6 +26,7 @@ import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.upstream.BandwidthMeter; @@ -109,7 +111,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; } @@ -118,9 +121,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; } /** @@ -175,6 +179,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/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index cf1ce09444..a50e1e1def 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/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.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Bundleable; @@ -66,14 +66,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 = @@ -140,7 +145,6 @@ public final class DefaultTrackSelectorTest { @Before public void setUp() { - initMocks(this); when(bandwidthMeter.getBitrateEstimate()).thenReturn(1000000L); Context context = ApplicationProvider.getApplicationContext(); defaultParameters = Parameters.getDefaults(context); @@ -875,11 +879,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( @@ -955,11 +966,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(); @@ -967,6 +980,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( @@ -1452,9 +1470,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( @@ -1474,6 +1550,10 @@ public final class DefaultTrackSelectorTest { /* channelCount= */ 6, MimeTypes.AUDIO_AAC, /* sampleRate= */ 44100)); + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setConstrainAudioChannelCountToDeviceCapabilities(false)); TrackSelectorResult result = trackSelector.selectTracks( @@ -1566,10 +1646,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. @@ -1590,7 +1677,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); @@ -1599,7 +1690,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); @@ -1608,7 +1703,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); @@ -1619,6 +1718,7 @@ public final class DefaultTrackSelectorTest { trackSelector.setParameters( defaultParameters .buildUpon() + .setConstrainAudioChannelCountToDeviceCapabilities(false) .setMaxAudioChannelCount(1) .setExceedAudioConstraintsIfNecessary(false)); result = @@ -2397,6 +2497,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)