diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e9ff8c8862c..cd5482c6cb5 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -19,6 +19,9 @@ * CompositionPlayer: * Transformer: * Track selection: + * Add `TrackSelectionParameters.preferredAudioChannelCount` to prefer + audio tracks with a specific channel count + ([#2996](https://github.com/androidx/media/pull/2996)). * Extractors: * Inspector: * Audio: 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 0e08358f4d6..bca95eee68d 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; @@ -660,6 +665,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. * @@ -1299,6 +1321,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). @@ -1422,6 +1451,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; @@ -1481,6 +1511,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) @@ -1527,6 +1558,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(); @@ -1591,6 +1623,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()} @@ -1633,6 +1666,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 ee9d690bef3..f304fb02db3 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 @@ -413,6 +413,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 @@ -1273,6 +1281,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) { @@ -4214,7 +4229,13 @@ 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 0bb85c78777..67744a25e03 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 @@ -688,6 +688,127 @@ 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()