diff --git a/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java b/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java index 68e5b3c54f..ad3b831dfb 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.trackselection; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.BundleableUtil.fromBundleNullableList; +import static com.google.android.exoplayer2.util.BundleableUtil.toBundleArrayList; import static com.google.common.base.MoreObjects.firstNonNull; import android.content.Context; @@ -23,23 +25,27 @@ import android.graphics.Point; import android.os.Bundle; import android.os.Looper; import android.view.accessibility.CaptioningManager; -import androidx.annotation.CallSuper; 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.MediaItem; import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.primitives.Ints; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import org.checkerframework.checker.initialization.qual.UnknownInitialization; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; /** * Constraint parameters for track selection. @@ -93,6 +99,7 @@ public class TrackSelectionParameters implements Bundleable { // General private boolean forceLowestBitrate; private boolean forceHighestSupportedBitrate; + private ImmutableMap trackSelectionOverrides; private ImmutableSet<@C.TrackType Integer> disabledTrackTypes; /** @@ -123,6 +130,7 @@ public class TrackSelectionParameters implements Bundleable { // General forceLowestBitrate = false; forceHighestSupportedBitrate = false; + trackSelectionOverrides = ImmutableMap.of(); disabledTrackTypes = ImmutableSet.of(); } @@ -224,7 +232,17 @@ public class TrackSelectionParameters implements Bundleable { bundle.getBoolean( keyForField(FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE), DEFAULT_WITHOUT_CONTEXT.forceHighestSupportedBitrate); - + List keys = + fromBundleNullableList( + TrackGroup.CREATOR, + bundle.getParcelableArrayList(keyForField(FIELD_SELECTION_OVERRIDE_KEYS)), + ImmutableList.of()); + List values = + fromBundleNullableList( + TrackSelectionOverride.CREATOR, + bundle.getParcelableArrayList(keyForField(FIELD_SELECTION_OVERRIDE_VALUES)), + ImmutableList.of()); + trackSelectionOverrides = zipToMap(keys, values); disabledTrackTypes = ImmutableSet.copyOf( Ints.asList( @@ -238,6 +256,7 @@ public class TrackSelectionParameters implements Bundleable { "preferredAudioLanguages", "preferredAudioMimeTypes", "preferredTextLanguages", + "trackSelectionOverrides", "disabledTrackTypes", }) private void init(@UnknownInitialization Builder this, TrackSelectionParameters parameters) { @@ -267,6 +286,7 @@ public class TrackSelectionParameters implements Bundleable { // General forceLowestBitrate = parameters.forceLowestBitrate; forceHighestSupportedBitrate = parameters.forceHighestSupportedBitrate; + trackSelectionOverrides = parameters.trackSelectionOverrides; disabledTrackTypes = parameters.disabledTrackTypes; } @@ -615,6 +635,18 @@ public class TrackSelectionParameters implements Bundleable { return this; } + /** + * Sets the selection overrides. + * + * @param trackSelectionOverrides The track selection overrides. + * @return This builder. + */ + public Builder setTrackSelectionOverrides( + Map trackSelectionOverrides) { + this.trackSelectionOverrides = ImmutableMap.copyOf(trackSelectionOverrides); + return this; + } + /** * Sets the disabled track types, preventing all tracks of those types from being selected for * playback. @@ -659,6 +691,83 @@ public class TrackSelectionParameters implements Bundleable { } return listBuilder.build(); } + + private static ImmutableMap<@NonNull K, @NonNull V> zipToMap( + List<@NonNull K> keys, List<@NonNull V> values) { + ImmutableMap.Builder<@NonNull K, @NonNull V> builder = new ImmutableMap.Builder<>(); + for (int i = 0; i < keys.size(); i++) { + builder.put(keys.get(i), values.get(i)); + } + return builder.build(); + } + } + + /** + * Forces the selection of {@link #tracks} for a {@link TrackGroup}. + * + * @see #trackSelectionOverrides + */ + public static final class TrackSelectionOverride implements Bundleable { + /** Force the selection of the associated {@link TrackGroup}, but no track will be played. */ + public static final TrackSelectionOverride DISABLE = + new TrackSelectionOverride(ImmutableSet.of()); + + /** The index of tracks in a {@link TrackGroup} to be selected. */ + public final ImmutableSet tracks; + + /** Constructs an instance to force {@code tracks} to be selected. */ + public TrackSelectionOverride(ImmutableSet tracks) { + this.tracks = tracks; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + TrackSelectionOverride that = (TrackSelectionOverride) obj; + return tracks.equals(that.tracks); + } + + @Override + public int hashCode() { + return tracks.hashCode(); + } + + // Bundleable implementation + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + FIELD_TRACKS, + }) + private @interface FieldNumber {} + + private static final int FIELD_TRACKS = 0; + + @Override + public Bundle toBundle() { + Bundle bundle = new Bundle(); + bundle.putIntArray(keyForField(FIELD_TRACKS), Ints.toArray(tracks)); + return bundle; + } + + /** Object that can restore {@code TrackSelectionOverride} from a {@link Bundle}. */ + public static final Creator CREATOR = + bundle -> { + @Nullable int[] tracks = bundle.getIntArray(keyForField(FIELD_TRACKS)); + if (tracks == null) { + return DISABLE; + } + return new TrackSelectionOverride(ImmutableSet.copyOf(Ints.asList(tracks))); + }; + + private static String keyForField(@FieldNumber int field) { + return Integer.toString(field, Character.MAX_RADIX); + } } /** @@ -810,6 +919,30 @@ public class TrackSelectionParameters implements Bundleable { * other constraints. The default value is {@code false}. */ public final boolean forceHighestSupportedBitrate; + + /** + * For each {@link TrackGroup} in the map, forces the tracks associated with it to be selected for + * playback. + * + *

For example if {@code trackSelectionOverrides.equals(ImmutableMap.of(trackGroup, + * ImmutableSet.of(1, 2, 3)))}, the tracks 1, 2 and 3 of {@code trackGroup} will be selected. + * + *

If multiple of the current {@link TrackGroup}s of the same {@link C.TrackType} are + * overridden, it is undetermined which one(s) will be selected. For example if a {@link + * MediaItem} has 2 video track groups (for example 2 different angles), and both are overriden, + * it is undetermined which one will be selected. + * + *

If multiple tracks of the {@link TrackGroup} are overriden, all supported (see {@link + * C.FormatSupport}) will be selected. + * + *

If a {@link TrackGroup} is associated with an empty set of tracks, no tracks will be played. + * This is similar to {@link #disabledTrackTypes}, except it will only affect the playback of the + * associated {@link TrackGroup}. For example, if the {@link C#TRACK_TYPE_VIDEO} {@link + * TrackGroup} is associated with no tracks, no video will play until the next video starts. + * + *

The default value is that no {@link TrackGroup} selections are overridden (empty map). + */ + public final ImmutableMap trackSelectionOverrides; /** * The track types that are disabled. No track of a disabled type will be selected, thus no track * type contained in the set will be played. The default value is that no track type is disabled @@ -844,6 +977,7 @@ public class TrackSelectionParameters implements Bundleable { // General this.forceLowestBitrate = builder.forceLowestBitrate; this.forceHighestSupportedBitrate = builder.forceHighestSupportedBitrate; + this.trackSelectionOverrides = builder.trackSelectionOverrides; this.disabledTrackTypes = builder.disabledTrackTypes; } @@ -887,6 +1021,7 @@ public class TrackSelectionParameters implements Bundleable { // General && forceLowestBitrate == other.forceLowestBitrate && forceHighestSupportedBitrate == other.forceHighestSupportedBitrate + && trackSelectionOverrides.equals(other.trackSelectionOverrides) && disabledTrackTypes.equals(other.disabledTrackTypes); } @@ -919,6 +1054,7 @@ public class TrackSelectionParameters implements Bundleable { // General result = 31 * result + (forceLowestBitrate ? 1 : 0); result = 31 * result + (forceHighestSupportedBitrate ? 1 : 0); + result = 31 * result + trackSelectionOverrides.hashCode(); result = 31 * result + disabledTrackTypes.hashCode(); return result; } @@ -950,6 +1086,8 @@ public class TrackSelectionParameters implements Bundleable { FIELD_PREFERRED_AUDIO_MIME_TYPES, FIELD_FORCE_LOWEST_BITRATE, FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE, + FIELD_SELECTION_OVERRIDE_KEYS, + FIELD_SELECTION_OVERRIDE_VALUES, FIELD_DISABLED_TRACK_TYPE, }) private @interface FieldNumber {} @@ -976,10 +1114,11 @@ public class TrackSelectionParameters implements Bundleable { private static final int FIELD_PREFERRED_AUDIO_MIME_TYPES = 20; private static final int FIELD_FORCE_LOWEST_BITRATE = 21; private static final int FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE = 22; - private static final int FIELD_DISABLED_TRACK_TYPE = 23; + private static final int FIELD_SELECTION_OVERRIDE_KEYS = 23; + private static final int FIELD_SELECTION_OVERRIDE_VALUES = 24; + private static final int FIELD_DISABLED_TRACK_TYPE = 25; @Override - @CallSuper public Bundle toBundle() { Bundle bundle = new Bundle(); @@ -1019,6 +1158,12 @@ public class TrackSelectionParameters implements Bundleable { bundle.putBoolean(keyForField(FIELD_FORCE_LOWEST_BITRATE), forceLowestBitrate); bundle.putBoolean( keyForField(FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE), forceHighestSupportedBitrate); + bundle.putParcelableArrayList( + keyForField(FIELD_SELECTION_OVERRIDE_KEYS), + toBundleArrayList(trackSelectionOverrides.keySet())); + bundle.putParcelableArrayList( + keyForField(FIELD_SELECTION_OVERRIDE_VALUES), + toBundleArrayList(trackSelectionOverrides.values())); bundle.putIntArray(keyForField(FIELD_DISABLED_TRACK_TYPE), Ints.toArray(disabledTrackTypes)); return bundle; diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/BundleableUtil.java b/library/common/src/main/java/com/google/android/exoplayer2/util/BundleableUtil.java index 978095354b..7b59db54e8 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/BundleableUtil.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/BundleableUtil.java @@ -24,6 +24,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.Bundleable; import com.google.common.collect.ImmutableList; import java.util.ArrayList; +import java.util.Collection; import java.util.List; /** Utilities for {@link Bundleable}. */ @@ -108,14 +109,15 @@ public final class BundleableUtil { } /** - * Converts a list of {@link Bundleable} to an {@link ArrayList} of {@link Bundle} so that the - * returned list can be put to {@link Bundle} using {@link Bundle#putParcelableArrayList} + * Converts a collection of {@link Bundleable} to an {@link ArrayList} of {@link Bundle} so that + * the returned list can be put to {@link Bundle} using {@link Bundle#putParcelableArrayList} * conveniently. */ - public static ArrayList toBundleArrayList(List bundleableList) { - ArrayList arrayList = new ArrayList<>(bundleableList.size()); - for (int i = 0; i < bundleableList.size(); i++) { - arrayList.add(bundleableList.get(i).toBundle()); + public static ArrayList toBundleArrayList( + Collection bundleables) { + ArrayList arrayList = new ArrayList<>(bundleables.size()); + for (T element : bundleables) { + arrayList.add(element.toBundle()); } return arrayList; } diff --git a/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionParametersTest.java b/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionParametersTest.java index 69743bb609..701d88d0e6 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionParametersTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectionParametersTest.java @@ -19,8 +19,14 @@ import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.Bundleable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters.TrackSelectionOverride; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import org.junit.Test; import org.junit.runner.RunWith; @@ -58,10 +64,16 @@ public class TrackSelectionParametersTest { // General assertThat(parameters.forceLowestBitrate).isFalse(); assertThat(parameters.forceHighestSupportedBitrate).isFalse(); + assertThat(parameters.trackSelectionOverrides).isEmpty(); + assertThat(parameters.disabledTrackTypes).isEmpty(); } @Test public void parametersSet_fromDefault_isAsExpected() { + ImmutableMap trackSelectionOverrides = + ImmutableMap.of( + new TrackGroup(new Format.Builder().build()), + new TrackSelectionOverride(/* tracks= */ ImmutableSet.of(2, 3))); TrackSelectionParameters parameters = TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT .buildUpon() @@ -90,6 +102,8 @@ public class TrackSelectionParametersTest { // General .setForceLowestBitrate(false) .setForceHighestSupportedBitrate(true) + .setTrackSelectionOverrides(trackSelectionOverrides) + .setDisabledTrackTypes(ImmutableSet.of(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_TEXT)) .build(); // Video @@ -122,6 +136,9 @@ public class TrackSelectionParametersTest { // General assertThat(parameters.forceLowestBitrate).isFalse(); assertThat(parameters.forceHighestSupportedBitrate).isTrue(); + assertThat(parameters.trackSelectionOverrides).isEqualTo(trackSelectionOverrides); + assertThat(parameters.disabledTrackTypes) + .containsExactly(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_TEXT); } @Test @@ -160,4 +177,17 @@ public class TrackSelectionParametersTest { assertThat(parameters.viewportHeight).isEqualTo(Integer.MAX_VALUE); assertThat(parameters.viewportOrientationMayChange).isTrue(); } + + /** Tests {@link SelectionOverride}'s {@link Bundleable} implementation. */ + @Test + public void roundTripViaBundle_ofSelectionOverride_yieldsEqualInstance() { + SelectionOverride selectionOverrideToBundle = + new SelectionOverride(/* groupIndex= */ 1, /* tracks...= */ 2, 3); + + SelectionOverride selectionOverrideFromBundle = + DefaultTrackSelector.SelectionOverride.CREATOR.fromBundle( + selectionOverrideToBundle.toBundle()); + + assertThat(selectionOverrideFromBundle).isEqualTo(selectionOverrideToBundle); + } } 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 b0112d34ae..b93a575626 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 @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.Timeline; 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.trackselection.TrackSelectionParameters.TrackSelectionOverride; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.BundleableUtil; import com.google.android.exoplayer2.util.Util; @@ -608,6 +609,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } + @Override + public ParametersBuilder setTrackSelectionOverrides( + Map trackSelectionOverrides) { + super.setTrackSelectionOverrides(trackSelectionOverrides); + return this; + } + @Override public ParametersBuilder setDisabledTrackTypes(Set<@C.TrackType Integer> disabledTrackTypes) { super.setDisabledTrackTypes(disabledTrackTypes); @@ -1490,19 +1498,33 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Apply track disabling and overriding. for (int i = 0; i < rendererCount; i++) { + // Per renderer and per track type disabling @C.TrackType int rendererType = mappedTrackInfo.getRendererType(i); if (params.getRendererDisabled(i) || params.disabledTrackTypes.contains(rendererType)) { definitions[i] = null; continue; } + // Per TrackGroupArray override TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(i); if (params.hasSelectionOverride(i, rendererTrackGroups)) { - SelectionOverride override = params.getSelectionOverride(i, rendererTrackGroups); + @Nullable SelectionOverride override = params.getSelectionOverride(i, rendererTrackGroups); definitions[i] = override == null ? null : new ExoTrackSelection.Definition( rendererTrackGroups.get(override.groupIndex), override.tracks, override.type); + continue; + } + // Per TrackGroup override + for (int j = 0; j < rendererTrackGroups.length; j++) { + TrackGroup trackGroup = rendererTrackGroups.get(j); + @Nullable + TrackSelectionOverride overrideTracks = params.trackSelectionOverrides.get(trackGroup); + if (overrideTracks != null) { + definitions[i] = + new ExoTrackSelection.Definition(trackGroup, Ints.toArray(overrideTracks.tracks)); + break; + } } } 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 5630d5d832..146e670798 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 @@ -45,10 +45,12 @@ import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.ParametersBuilder; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters.TrackSelectionOverride; import com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationListener; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.util.HashMap; import java.util.Map; @@ -152,16 +154,21 @@ public final class DefaultTrackSelectorTest { assertThat(parametersFromBundle).isEqualTo(parametersToBundle); } - /** Tests {@link SelectionOverride}'s {@link Bundleable} implementation. */ + /** Tests that an empty override clears a track selection. */ @Test - public void roundTripViaBundle_ofSelectionOverride_yieldsEqualInstance() { - SelectionOverride selectionOverrideToBundle = - new SelectionOverride(/* groupIndex= */ 1, /* tracks...= */ 2, 3); + public void selectTracks_withNullOverride_clearsTrackSelection() throws ExoPlaybackException { + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setTrackSelectionOverrides( + ImmutableMap.of(VIDEO_TRACK_GROUP, new TrackSelectionOverride(ImmutableSet.of())))); - SelectionOverride selectionOverrideFromBundle = - SelectionOverride.CREATOR.fromBundle(selectionOverrideToBundle.toBundle()); + TrackSelectorResult result = + trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS, periodId, TIMELINE); - assertThat(selectionOverrideFromBundle).isEqualTo(selectionOverrideToBundle); + assertSelections(result, new TrackSelection[] {null, TRACK_SELECTIONS[1]}); + assertThat(result.rendererConfigurations) + .isEqualTo(new RendererConfiguration[] {null, DEFAULT}); } /** Tests that a null override clears a track selection. */ @@ -193,6 +200,29 @@ public final class DefaultTrackSelectorTest { .isEqualTo(new RendererConfiguration[] {DEFAULT, DEFAULT}); } + /** Tests that an empty override is not applied for a different set of available track groups. */ + @Test + public void selectTracks_withEmptyTrackOverrideForDifferentTracks_hasNoEffect() + throws ExoPlaybackException { + trackSelector.setParameters( + trackSelector + .buildUponParameters() + .setTrackSelectionOverrides( + ImmutableMap.of( + new TrackGroup(VIDEO_FORMAT, VIDEO_FORMAT), TrackSelectionOverride.DISABLE))); + + TrackSelectorResult result = + trackSelector.selectTracks( + RENDERER_CAPABILITIES, + new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, VIDEO_TRACK_GROUP), + periodId, + TIMELINE); + + assertThat(result.selections).asList().containsExactlyElementsIn(TRACK_SELECTIONS).inOrder(); + assertThat(result.rendererConfigurations) + .isEqualTo(new RendererConfiguration[] {DEFAULT, DEFAULT}); + } + /** Tests that an override is not applied for a different set of available track groups. */ @Test public void selectTracksWithNullOverrideForDifferentTracks() throws ExoPlaybackException { @@ -1815,6 +1845,10 @@ public final class DefaultTrackSelectorTest { .setRendererDisabled(1, true) .setRendererDisabled(3, true) .setRendererDisabled(5, false) + .setTrackSelectionOverrides( + ImmutableMap.of( + AUDIO_TRACK_GROUP, + new TrackSelectionOverride(/* tracks= */ ImmutableSet.of(3, 4, 5)))) .setDisabledTrackTypes(ImmutableSet.of(C.TRACK_TYPE_AUDIO)) .build(); }