diff --git a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java index 520201344b..73aef3918a 100644 --- a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java +++ b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java @@ -96,6 +96,7 @@ public static class Builder { private ImmutableList preferredAudioLabels; private @C.RoleFlags int preferredAudioRoleFlags; private int maxAudioChannelCount; + private int preferredAudioChannelCount; private int maxAudioBitrate; private ImmutableList preferredAudioMimeTypes; private AudioOffloadPreferences audioOffloadPreferences; @@ -135,6 +136,7 @@ public Builder() { preferredAudioLabels = ImmutableList.of(); preferredAudioRoleFlags = 0; maxAudioChannelCount = Integer.MAX_VALUE; + preferredAudioChannelCount = 0; maxAudioBitrate = Integer.MAX_VALUE; preferredAudioMimeTypes = ImmutableList.of(); audioOffloadPreferences = AudioOffloadPreferences.DEFAULT; @@ -215,6 +217,8 @@ protected Builder(Bundle bundle) { bundle.getInt(FIELD_PREFERRED_AUDIO_ROLE_FLAGS, DEFAULT.preferredAudioRoleFlags); maxAudioChannelCount = bundle.getInt(FIELD_MAX_AUDIO_CHANNEL_COUNT, DEFAULT.maxAudioChannelCount); + preferredAudioChannelCount = + bundle.getInt(FIELD_PREFERRED_AUDIO_CHANNEL_COUNT, DEFAULT.preferredAudioChannelCount); maxAudioBitrate = bundle.getInt(FIELD_MAX_AUDIO_BITRATE, DEFAULT.maxAudioBitrate); preferredAudioMimeTypes = ImmutableList.copyOf( @@ -331,6 +335,7 @@ private void init(@UnknownInitialization Builder this, TrackSelectionParameters preferredAudioRoleFlags = parameters.preferredAudioRoleFlags; preferredAudioLabels = parameters.preferredAudioLabels; maxAudioChannelCount = parameters.maxAudioChannelCount; + preferredAudioChannelCount = parameters.preferredAudioChannelCount; maxAudioBitrate = parameters.maxAudioBitrate; preferredAudioMimeTypes = parameters.preferredAudioMimeTypes; audioOffloadPreferences = parameters.audioOffloadPreferences; @@ -664,6 +669,23 @@ public Builder setMaxAudioChannelCount(int maxAudioChannelCount) { return this; } + /** + * Sets the preferred audio channel count. When set to a value greater than 0, audio tracks + * with channel count >= this value will be preferred over tracks with lower channel count, + * regardless of role flags (main vs alt). This allows selecting 5.1 surround tracks over + * stereo tracks even when the stereo track has a main role. + * + *

Set to 0 to disable this preference (default behavior based on role flags). + * + * @param preferredAudioChannelCount Preferred audio channel count, or 0 to disable. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setPreferredAudioChannelCount(int preferredAudioChannelCount) { + this.preferredAudioChannelCount = preferredAudioChannelCount; + return this; + } + /** * Sets the maximum allowed audio bitrate. * @@ -1305,6 +1327,13 @@ public static TrackSelectionParameters getDefaults(Context context) { */ public final int maxAudioChannelCount; + /** + * Preferred audio channel count. When set to a value greater than 0, audio tracks with channel + * count >= this value will be preferred over tracks with lower channel count, regardless of role + * flags. The default value is {@code 0} (disabled). + */ + public final int preferredAudioChannelCount; + /** * Maximum allowed audio bitrate in bits per second. The default value is {@link * Integer#MAX_VALUE} (i.e. no constraint). @@ -1428,6 +1457,7 @@ protected TrackSelectionParameters(Builder builder) { this.preferredAudioLanguages = builder.preferredAudioLanguages; this.preferredAudioRoleFlags = builder.preferredAudioRoleFlags; this.maxAudioChannelCount = builder.maxAudioChannelCount; + this.preferredAudioChannelCount = builder.preferredAudioChannelCount; this.preferredAudioLabels = builder.preferredAudioLabels; this.maxAudioBitrate = builder.maxAudioBitrate; this.preferredAudioMimeTypes = builder.preferredAudioMimeTypes; @@ -1487,6 +1517,7 @@ public boolean equals(@Nullable Object obj) { && preferredAudioLanguages.equals(other.preferredAudioLanguages) && preferredAudioRoleFlags == other.preferredAudioRoleFlags && maxAudioChannelCount == other.maxAudioChannelCount + && preferredAudioChannelCount == other.preferredAudioChannelCount && preferredAudioLabels.equals(other.preferredAudioLabels) && maxAudioBitrate == other.maxAudioBitrate && preferredAudioMimeTypes.equals(other.preferredAudioMimeTypes) @@ -1533,6 +1564,7 @@ public int hashCode() { result = 31 * result + preferredAudioLanguages.hashCode(); result = 31 * result + preferredAudioRoleFlags; result = 31 * result + maxAudioChannelCount; + result = 31 * result + preferredAudioChannelCount; result = 31 * result + preferredAudioLabels.hashCode(); result = 31 * result + maxAudioBitrate; result = 31 * result + preferredAudioMimeTypes.hashCode(); @@ -1597,6 +1629,7 @@ public int hashCode() { private static final String FIELD_PREFERRED_VIDEO_LABELS = Util.intToStringMaxRadix(36); private static final String FIELD_PREFERRED_AUDIO_LABELS = Util.intToStringMaxRadix(37); private static final String FIELD_PREFERRED_TEXT_LABELS = Util.intToStringMaxRadix(38); + private static final String FIELD_PREFERRED_AUDIO_CHANNEL_COUNT = Util.intToStringMaxRadix(39); /** * Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()} @@ -1639,6 +1672,7 @@ public Bundle toBundle() { FIELD_PREFERRED_AUDIO_LANGUAGES, preferredAudioLanguages.toArray(new String[0])); bundle.putInt(FIELD_PREFERRED_AUDIO_ROLE_FLAGS, preferredAudioRoleFlags); bundle.putInt(FIELD_MAX_AUDIO_CHANNEL_COUNT, maxAudioChannelCount); + bundle.putInt(FIELD_PREFERRED_AUDIO_CHANNEL_COUNT, preferredAudioChannelCount); bundle.putInt(FIELD_MAX_AUDIO_BITRATE, maxAudioBitrate); bundle.putStringArray( FIELD_PREFERRED_AUDIO_LABELS, preferredAudioLabels.toArray(new String[0])); 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 614abee46e..a21bfef4c6 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 @@ -417,6 +417,14 @@ public ParametersBuilder setMaxAudioChannelCount(int maxAudioChannelCount) { return this; } + @SuppressWarnings("deprecation") // Intentionally returning deprecated type + @CanIgnoreReturnValue + @Override + public ParametersBuilder setPreferredAudioChannelCount(int preferredAudioChannelCount) { + delegate.setPreferredAudioChannelCount(preferredAudioChannelCount); + return this; + } + @SuppressWarnings("deprecation") // Intentionally returning deprecated type @CanIgnoreReturnValue @Override @@ -1277,6 +1285,13 @@ public Builder setMaxAudioChannelCount(int maxAudioChannelCount) { return this; } + @CanIgnoreReturnValue + @Override + public Builder setPreferredAudioChannelCount(int preferredAudioChannelCount) { + super.setPreferredAudioChannelCount(preferredAudioChannelCount); + return this; + } + @CanIgnoreReturnValue @Override public Builder setMaxAudioBitrate(int maxAudioBitrate) { @@ -4074,7 +4089,12 @@ public int compareTo(AudioTrackInfo other) { this.preferredLanguageIndex, other.preferredLanguageIndex, Ordering.natural().reverse()) - .compare(this.preferredLanguageScore, other.preferredLanguageScore) + .compare(this.preferredLanguageScore, other.preferredLanguageScore); + // Compare preferred channel count (when set, higher channel count wins over role) + if (this.parameters.preferredAudioChannelCount > 0) { + comparisonChain = comparisonChain.compare(this.channelCount, other.channelCount); + } + comparisonChain = comparisonChain .compare(this.preferredRoleFlagsScore, other.preferredRoleFlagsScore) .compare( this.preferredLabelMatchIndex, 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 2ca1bc7f9c..e3825c0c01 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 @@ -685,6 +685,121 @@ public void selectTracks_withPreferredAudioRoleFlags_selectPreferredTrack() thro assertFixedSelection(result.selections[0], trackGroups, lessRoleFlags); } + /** + * Tests that track selector will select the audio track with higher channel count when {@link + * Parameters#preferredAudioChannelCount} is set, even if the track has an alternate role flag. + */ + @Test + public void selectTracks_withPreferredAudioChannelCount_selectsHigherChannelCountTrack() + throws Exception { + Format stereoMainFormat = + AUDIO_FORMAT.buildUpon() + .setId("stereo_main") + .setChannelCount(2) + .setRoleFlags(C.ROLE_FLAG_MAIN) + .build(); + Format surroundAltFormat = + AUDIO_FORMAT.buildUpon() + .setId("surround_alt") + .setChannelCount(6) + .setRoleFlags(C.ROLE_FLAG_ALTERNATE) + .build(); + TrackGroupArray trackGroups = wrapFormats(stereoMainFormat, surroundAltFormat); + + // Without preferredAudioChannelCount, main role stereo track should be selected + trackSelector.setParameters(defaultParameters); + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, + trackGroups, + periodId, + TIMELINE); + assertFixedSelection(result.selections[0], trackGroups, stereoMainFormat); + + // With preferredAudioChannelCount=6, 5.1 surround track should be selected over stereo main + trackSelector.setParameters( + defaultParameters.buildUpon().setPreferredAudioChannelCount(6).build()); + result = + trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, + trackGroups, + periodId, + TIMELINE); + assertFixedSelection(result.selections[0], trackGroups, surroundAltFormat); + } + + /** + * Tests that track selector falls back to lower channel count track when preferred channel count + * track is not supported by renderer. + */ + @Test + public void selectTracks_withPreferredAudioChannelCountNotSupported_fallsBackToSupportedTrack() + throws Exception { + Format stereoFormat = + AUDIO_FORMAT.buildUpon() + .setId("stereo") + .setChannelCount(2) + .setRoleFlags(C.ROLE_FLAG_MAIN) + .build(); + Format surroundFormat = + AUDIO_FORMAT.buildUpon() + .setId("surround") + .setChannelCount(6) + .setRoleFlags(C.ROLE_FLAG_ALTERNATE) + .build(); + TrackGroupArray trackGroups = wrapFormats(stereoFormat, surroundFormat); + + // Renderer only supports stereo (2 channels), surround exceeds capabilities + Map mappedCapabilities = new HashMap<>(); + mappedCapabilities.put(stereoFormat.id, FORMAT_HANDLED); + mappedCapabilities.put(surroundFormat.id, FORMAT_EXCEEDS_CAPABILITIES); + RendererCapabilities stereoOnlyCapabilities = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities); + + // With preferredAudioChannelCount=6, but renderer doesn't support 6 channels, + // should fall back to stereo + trackSelector.setParameters( + defaultParameters.buildUpon().setPreferredAudioChannelCount(6).build()); + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {stereoOnlyCapabilities}, trackGroups, periodId, TIMELINE); + assertFixedSelection(result.selections[0], trackGroups, stereoFormat); + } + + /** + * Tests that track selector selects the track with highest channel count among supported tracks + * when preferredAudioChannelCount is set, even if no track meets the threshold. + */ + @Test + public void selectTracks_withPreferredAudioChannelCount_selectsHighestSupportedChannelCount() + throws Exception { + Format stereoFormat = + AUDIO_FORMAT.buildUpon() + .setId("stereo") + .setChannelCount(2) + .setRoleFlags(C.ROLE_FLAG_MAIN) + .build(); + Format surroundFormat = + AUDIO_FORMAT.buildUpon() + .setId("surround") + .setChannelCount(6) + .setRoleFlags(C.ROLE_FLAG_ALTERNATE) + .build(); + TrackGroupArray trackGroups = wrapFormats(stereoFormat, surroundFormat); + + // With preferredAudioChannelCount=8, but only 2ch and 6ch available, + // should select 6ch (highest available) even though it doesn't meet the threshold + trackSelector.setParameters( + defaultParameters.buildUpon().setPreferredAudioChannelCount(8).build()); + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, + trackGroups, + periodId, + TIMELINE); + assertFixedSelection(result.selections[0], trackGroups, surroundFormat); + } + @Test public void selectTracks_withPreferredTextLanguagesAndRoleFlagsFromCaptioningManager_selectsCaptioningTrack()