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 982268dfed..0629681806 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 @@ -1443,9 +1443,32 @@ public class DefaultTrackSelector extends MappingTrackSelector { rendererMixedMimeTypeAdaptationSupports, params); - // Apply track disabling and overriding. + // Apply per track type overrides. + SparseArray> applicableOverridesByTrackType = + getApplicableOverrides(mappedTrackInfo, params); + for (int i = 0; i < applicableOverridesByTrackType.size(); i++) { + Pair overrideAndRendererIndex = + applicableOverridesByTrackType.valueAt(i); + applyTrackTypeOverride( + mappedTrackInfo, + definitions, + /* trackType= */ applicableOverridesByTrackType.keyAt(i), + /* override= */ overrideAndRendererIndex.first, + /* overrideRendererIndex= */ overrideAndRendererIndex.second); + } + + // Apply legacy per renderer overrides. for (int i = 0; i < rendererCount; i++) { - definitions[i] = maybeApplyOverride(mappedTrackInfo, params, i, definitions[i]); + if (hasLegacyRendererOverride(mappedTrackInfo, params, /* rendererIndex= */ i)) { + definitions[i] = getLegacyRendererOverride(mappedTrackInfo, params, /* rendererIndex= */ i); + } + } + + // Disable renderers if needed. + for (int i = 0; i < rendererCount; i++) { + if (isRendererDisabled(mappedTrackInfo, params, /* rendererIndex= */ i)) { + definitions[i] = null; + } } @NullableType @@ -1477,53 +1500,94 @@ public class DefaultTrackSelector extends MappingTrackSelector { return Pair.create(rendererConfigurations, rendererTrackSelections); } - /** - * Returns the {@link ExoTrackSelection.Definition} of a renderer after applying selection - * overriding and renderer disabling. - */ - protected ExoTrackSelection.@NullableType Definition maybeApplyOverride( - MappedTrackInfo mappedTrackInfo, - Parameters params, - int rendererIndex, - ExoTrackSelection.@NullableType Definition currentDefinition) { - // Per renderer and per track type disabling + private boolean isRendererDisabled( + MappedTrackInfo mappedTrackInfo, Parameters params, int rendererIndex) { @C.TrackType int rendererType = mappedTrackInfo.getRendererType(rendererIndex); - if (params.getRendererDisabled(rendererIndex) - || params.disabledTrackTypes.contains(rendererType)) { + return params.getRendererDisabled(rendererIndex) + || params.disabledTrackTypes.contains(rendererType); + } + + @SuppressWarnings("deprecation") // Calling deprecated hasSelectionOverride. + private boolean hasLegacyRendererOverride( + MappedTrackInfo mappedTrackInfo, Parameters params, int rendererIndex) { + TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); + return params.hasSelectionOverride(rendererIndex, rendererTrackGroups); + } + + @SuppressWarnings("deprecation") // Calling deprecated getSelectionOverride. + private ExoTrackSelection.@NullableType Definition getLegacyRendererOverride( + MappedTrackInfo mappedTrackInfo, Parameters params, int rendererIndex) { + TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); + @Nullable + SelectionOverride override = params.getSelectionOverride(rendererIndex, rendererTrackGroups); + if (override == null) { return null; } - // Per TrackGroupArray overrides. - TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); - if (params.hasSelectionOverride(rendererIndex, rendererTrackGroups)) { - @Nullable - SelectionOverride override = params.getSelectionOverride(rendererIndex, rendererTrackGroups); - if (override == null) { - return null; - } - return new ExoTrackSelection.Definition( - rendererTrackGroups.get(override.groupIndex), override.tracks, override.type); - } - // Per TrackGroup overrides. - for (int j = 0; j < rendererTrackGroups.length; j++) { - TrackGroup trackGroup = rendererTrackGroups.get(j); - @Nullable - TrackSelectionOverride overrideTracks = - params.trackSelectionOverrides.getOverride(trackGroup); - if (overrideTracks != null) { - if (overrideTracks.trackIndices.isEmpty()) { - // TrackGroup is disabled. Deselect the currentDefinition if applicable. Otherwise ignore. - if (currentDefinition != null && currentDefinition.group.equals(trackGroup)) { - currentDefinition = null; - } - } else { - // Override current definition with new selection. - currentDefinition = - new ExoTrackSelection.Definition( - trackGroup, Ints.toArray(overrideTracks.trackIndices)); - } + return new ExoTrackSelection.Definition( + rendererTrackGroups.get(override.groupIndex), override.tracks, override.type); + } + + /** + * Returns applicable overrides. Mapping from track type to a pair of override and renderer index + * for this override. + */ + private SparseArray> getApplicableOverrides( + MappedTrackInfo mappedTrackInfo, Parameters params) { + SparseArray> applicableOverrides = new SparseArray<>(); + // Iterate through all existing track groups to ensure only overrides for those groups are used. + int rendererCount = mappedTrackInfo.getRendererCount(); + for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { + TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); + for (int j = 0; j < rendererTrackGroups.length; j++) { + maybeUpdateApplicableOverrides( + applicableOverrides, + params.trackSelectionOverrides.getOverride(rendererTrackGroups.get(j)), + rendererIndex); + } + } + // Also iterate unmapped groups to see if they have overrides. + TrackGroupArray unmappedGroups = mappedTrackInfo.getUnmappedTrackGroups(); + for (int i = 0; i < unmappedGroups.length; i++) { + maybeUpdateApplicableOverrides( + applicableOverrides, + params.trackSelectionOverrides.getOverride(unmappedGroups.get(i)), + /* rendererIndex= */ C.INDEX_UNSET); + } + return applicableOverrides; + } + + private void maybeUpdateApplicableOverrides( + SparseArray> applicableOverrides, + @Nullable TrackSelectionOverride override, + int rendererIndex) { + if (override == null) { + return; + } + @C.TrackType int trackType = override.getTrackType(); + @Nullable + Pair existingOverride = applicableOverrides.get(trackType); + if (existingOverride == null || existingOverride.first.trackIndices.isEmpty()) { + // We only need to choose one non-empty override per type. + applicableOverrides.put(trackType, Pair.create(override, rendererIndex)); + } + } + + private void applyTrackTypeOverride( + MappedTrackInfo mappedTrackInfo, + ExoTrackSelection.@NullableType Definition[] definitions, + @C.TrackType int trackType, + TrackSelectionOverride override, + int overrideRendererIndex) { + for (int i = 0; i < definitions.length; i++) { + if (overrideRendererIndex == i) { + definitions[i] = + new ExoTrackSelection.Definition( + override.trackGroup, Ints.toArray(override.trackIndices)); + } else if (mappedTrackInfo.getRendererType(i) == trackType) { + // Disable other renderers of the same type. + definitions[i] = null; } } - return currentDefinition; } // Track selection prior to overrides and disabled flags being applied. 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 c7b33310fc..ff2d0a01ba 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 @@ -18,6 +18,7 @@ package androidx.media3.exoplayer.trackselection; import static androidx.media3.common.C.FORMAT_EXCEEDS_CAPABILITIES; import static androidx.media3.common.C.FORMAT_HANDLED; import static androidx.media3.common.C.FORMAT_UNSUPPORTED_SUBTYPE; +import static androidx.media3.common.C.FORMAT_UNSUPPORTED_TYPE; import static androidx.media3.exoplayer.RendererCapabilities.ADAPTIVE_NOT_SEAMLESS; import static androidx.media3.exoplayer.RendererCapabilities.TUNNELING_NOT_SUPPORTED; import static androidx.media3.exoplayer.RendererConfiguration.DEFAULT; @@ -54,6 +55,7 @@ import androidx.media3.test.utils.FakeTimeline; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.util.HashMap; import java.util.Map; @@ -240,7 +242,8 @@ public final class DefaultTrackSelectorTest { assertThat(result.selections) .asList() - .containsExactly(new FixedTrackSelection(videoGroupMidBitrate, /* track= */ 0), null); + .containsExactly(new FixedTrackSelection(videoGroupMidBitrate, /* track= */ 0), null) + .inOrder(); } /** Tests that an empty override is not applied for a different set of available track groups. */ @@ -269,6 +272,97 @@ public final class DefaultTrackSelectorTest { .isEqualTo(new RendererConfiguration[] {DEFAULT, DEFAULT}); } + @Test + public void selectTrack_withOverrideForDifferentRenderer_clearsDefaultSelectionOfSameType() + throws Exception { + Format videoFormatH264 = + VIDEO_FORMAT.buildUpon().setId("H264").setSampleMimeType(MimeTypes.VIDEO_H264).build(); + Format videoFormatAv1 = + VIDEO_FORMAT.buildUpon().setId("AV1").setSampleMimeType(MimeTypes.VIDEO_AV1).build(); + TrackGroup videoGroupH264 = new TrackGroup(videoFormatH264); + TrackGroup videoGroupAv1 = new TrackGroup(videoFormatAv1); + Map rendererCapabilitiesMap = + ImmutableMap.of( + videoFormatH264.id, FORMAT_HANDLED, videoFormatAv1.id, FORMAT_UNSUPPORTED_TYPE); + RendererCapabilities rendererCapabilitiesH264 = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_VIDEO, rendererCapabilitiesMap); + rendererCapabilitiesMap = + ImmutableMap.of( + videoFormatH264.id, FORMAT_UNSUPPORTED_TYPE, videoFormatAv1.id, FORMAT_HANDLED); + RendererCapabilities rendererCapabilitiesAv1 = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_VIDEO, rendererCapabilitiesMap); + + // Try to force selection of one TrackGroup in both directions to ensure the default gets + // overridden without having to know what the default is. + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setTrackSelectionOverrides( + new TrackSelectionOverrides.Builder() + .setOverrideForType(new TrackSelectionOverride(videoGroupH264)) + .build())); + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {rendererCapabilitiesH264, rendererCapabilitiesAv1}, + new TrackGroupArray(videoGroupH264, videoGroupAv1), + periodId, + TIMELINE); + + assertThat(result.selections) + .asList() + .containsExactly(new FixedTrackSelection(videoGroupH264, /* track= */ 0), null) + .inOrder(); + + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setTrackSelectionOverrides( + new TrackSelectionOverrides.Builder() + .setOverrideForType(new TrackSelectionOverride(videoGroupAv1)) + .build())); + result = + trackSelector.selectTracks( + new RendererCapabilities[] {rendererCapabilitiesH264, rendererCapabilitiesAv1}, + new TrackGroupArray(videoGroupH264, videoGroupAv1), + periodId, + TIMELINE); + + assertThat(result.selections) + .asList() + .containsExactly(null, new FixedTrackSelection(videoGroupAv1, /* track= */ 0)) + .inOrder(); + } + + @Test + public void selectTracks_withOverrideForUnmappedGroup_disablesAllRenderersOfSameType() + throws Exception { + Format audioSupported = AUDIO_FORMAT.buildUpon().setId("supported").build(); + Format audioUnsupported = AUDIO_FORMAT.buildUpon().setId("unsupported").build(); + TrackGroup audioGroupSupported = new TrackGroup(audioSupported); + TrackGroup audioGroupUnsupported = new TrackGroup(audioUnsupported); + Map audioRendererCapabilitiesMap = + ImmutableMap.of( + audioSupported.id, FORMAT_HANDLED, audioUnsupported.id, FORMAT_UNSUPPORTED_TYPE); + RendererCapabilities audioRendererCapabilties = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, audioRendererCapabilitiesMap); + + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setTrackSelectionOverrides( + new TrackSelectionOverrides.Builder() + .setOverrideForType(new TrackSelectionOverride(audioGroupUnsupported)) + .build())); + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {VIDEO_CAPABILITIES, audioRendererCapabilties}, + new TrackGroupArray(VIDEO_TRACK_GROUP, audioGroupSupported, audioGroupUnsupported), + periodId, + TIMELINE); + + assertThat(result.selections).asList().containsExactly(VIDEO_TRACK_SELECTION, null).inOrder(); + } + /** Tests that an override is not applied for a different set of available track groups. */ @Test public void selectTracksWithNullOverrideForDifferentTracks() throws ExoPlaybackException { diff --git a/libraries/ui/src/main/java/androidx/media3/ui/StyledPlayerControlView.java b/libraries/ui/src/main/java/androidx/media3/ui/StyledPlayerControlView.java index 20a7ef5475..f7ac247886 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/StyledPlayerControlView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/StyledPlayerControlView.java @@ -83,7 +83,6 @@ import java.util.Formatter; import java.util.List; import java.util.Locale; import java.util.concurrent.CopyOnWriteArrayList; -import org.checkerframework.dataflow.qual.Pure; /** * A view for controlling {@link Player} instances. @@ -2168,12 +2167,11 @@ public class StyledPlayerControlView extends FrameLayout { TrackSelectionParameters trackSelectionParameters = player.getTrackSelectionParameters(); TrackSelectionOverrides overrides = - forceTrackSelection( - trackSelectionParameters.trackSelectionOverrides, - track.tracksInfo, - track.trackGroupIndex, - new TrackSelectionOverride( - track.trackGroup, ImmutableList.of(track.trackIndex))); + new TrackSelectionOverrides.Builder() + .setOverrideForType( + new TrackSelectionOverride( + track.trackGroup, ImmutableList.of(track.trackIndex))) + .build(); checkNotNull(player) .setTrackSelectionParameters( trackSelectionParameters @@ -2211,41 +2209,4 @@ public class StyledPlayerControlView extends FrameLayout { checkView = itemView.findViewById(R.id.exo_check); } } - - /** - * Forces tracks in a {@link TrackGroup} to be the only ones selected for a {@link C.TrackType}. - * No other tracks of that type will be selectable. If the forced tracks are not supported, then - * no tracks of that type will be selected. - * - * @param trackSelectionOverrides The current {@link TrackSelectionOverride overrides}. - * @param tracksInfo The current {@link TracksInfo}. - * @param forcedTrackGroupIndex The index of the {@link TrackGroup} in {@code tracksInfo} that - * should have its track selected. - * @param forcedTrackSelectionOverride The tracks to force selection of. - * @return The updated {@link TrackSelectionOverride overrides}. - */ - @Pure - private static TrackSelectionOverrides forceTrackSelection( - TrackSelectionOverrides trackSelectionOverrides, - TracksInfo tracksInfo, - int forcedTrackGroupIndex, - TrackSelectionOverride forcedTrackSelectionOverride) { - TrackSelectionOverrides.Builder overridesBuilder = trackSelectionOverrides.buildUpon(); - - @C.TrackType - int trackType = tracksInfo.getTrackGroupInfos().get(forcedTrackGroupIndex).getTrackType(); - overridesBuilder.setOverrideForType(forcedTrackSelectionOverride); - // TrackSelectionOverride doesn't currently guarantee that only overwritten track - // group of a given type are selected, so the others have to be explicitly disabled. - // This guarantee is provided in the following patch that removes the need for this method. - ImmutableList trackGroupInfos = tracksInfo.getTrackGroupInfos(); - for (int i = 0; i < trackGroupInfos.size(); i++) { - TrackGroupInfo trackGroupInfo = trackGroupInfos.get(i); - if (i != forcedTrackGroupIndex && trackGroupInfo.getTrackType() == trackType) { - TrackGroup trackGroup = trackGroupInfo.getTrackGroup(); - overridesBuilder.addOverride(new TrackSelectionOverride(trackGroup, ImmutableList.of())); - } - } - return overridesBuilder.build(); - } }