From ac881be2fccbbfe29fe3b4f094b85d6d16942c72 Mon Sep 17 00:00:00 2001 From: krocard Date: Tue, 5 Oct 2021 11:16:12 +0100 Subject: [PATCH] Add TracksInfo to the Player API TracksInfo is very similar to `MappingTrackSelector.MappedTracksInfo` with some fields removed to simplify the Player API, notably it doesn't expose the renderer concept. A significant difference is the addition of a `selected` boolean field which avoids having a separate `getCurrentTrackSelection` API. This cl is a part of the bigger track selection change, splitted for ease of review. In particular, the MediaSession implementation and UI usage have been slitted in child cls. Find all cls with the tag: #player-track-selection PiperOrigin-RevId: 400937124 --- .../exoplayer2/ext/cast/CastPlayer.java | 44 ++- .../exoplayer2/ext/ima/FakePlayer.java | 6 + .../android/exoplayer2/ForwardingPlayer.java | 10 + .../com/google/android/exoplayer2/Player.java | 49 ++- .../google/android/exoplayer2/TracksInfo.java | 300 ++++++++++++++++++ .../android/exoplayer2/TracksInfoTest.java | 111 +++++++ .../android/exoplayer2/ExoPlayerImpl.java | 12 +- .../android/exoplayer2/SimpleExoPlayer.java | 6 + .../analytics/AnalyticsCollector.java | 11 + .../analytics/AnalyticsListener.java | 14 +- .../trackselection/MappingTrackSelector.java | 58 +++- .../trackselection/TrackSelectorResult.java | 24 ++ .../android/exoplayer2/ExoPlayerTest.java | 5 +- .../exoplayer2/MediaPeriodQueueTest.java | 5 +- .../analytics/AnalyticsCollectorTest.java | 6 +- .../DefaultTrackSelectorTest.java | 28 ++ .../MappingTrackSelectorTest.java | 58 ++++ .../exoplayer2/transformer/Transformer.java | 6 +- .../android/exoplayer2/ui/PlayerView.java | 4 +- .../exoplayer2/ui/StyledPlayerView.java | 4 +- .../testutil/ExoPlayerTestRunner.java | 19 -- .../exoplayer2/testutil/StubExoPlayer.java | 6 + 22 files changed, 727 insertions(+), 59 deletions(-) create mode 100644 library/common/src/main/java/com/google/android/exoplayer2/TracksInfo.java create mode 100644 library/common/src/test/java/com/google/android/exoplayer2/TracksInfoTest.java diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 22dc83818e..e3f075e562 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -37,6 +37,7 @@ import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.TracksInfo; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -66,6 +67,7 @@ import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.ResultCallback; import com.google.common.collect.ImmutableList; import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** @@ -101,7 +103,8 @@ public final class CastPlayer extends BasePlayer { COMMAND_GET_TIMELINE, COMMAND_GET_MEDIA_ITEMS_METADATA, COMMAND_SET_MEDIA_ITEMS_METADATA, - COMMAND_CHANGE_MEDIA_ITEMS) + COMMAND_CHANGE_MEDIA_ITEMS, + COMMAND_GET_TRACK_INFOS) .build(); public static final float MIN_SPEED_SUPPORTED = 0.5f; @@ -142,6 +145,7 @@ public final class CastPlayer extends BasePlayer { private CastTimeline currentTimeline; private TrackGroupArray currentTrackGroups; private TrackSelectionArray currentTrackSelection; + private TracksInfo currentTracksInfo; private Commands availableCommands; @Player.State private int playbackState; private int currentWindowIndex; @@ -219,6 +223,7 @@ public final class CastPlayer extends BasePlayer { currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE; currentTrackGroups = TrackGroupArray.EMPTY; currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY; + currentTracksInfo = TracksInfo.EMPTY; availableCommands = new Commands.Builder().addAll(PERMANENT_AVAILABLE_COMMANDS).build(); pendingSeekWindowIndex = C.INDEX_UNSET; pendingSeekPositionMs = C.TIME_UNSET; @@ -583,14 +588,19 @@ public final class CastPlayer extends BasePlayer { return false; } + @Override + public TrackGroupArray getCurrentTrackGroups() { + return currentTrackGroups; + } + @Override public TrackSelectionArray getCurrentTrackSelections() { return currentTrackSelection; } @Override - public TrackGroupArray getCurrentTrackGroups() { - return currentTrackGroups; + public TracksInfo getCurrentTracksInfo() { + return currentTracksInfo; } @Override @@ -871,6 +881,8 @@ public final class CastPlayer extends BasePlayer { listeners.queueEvent( Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection)); + listeners.queueEvent( + Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksInfoChanged(currentTracksInfo)); } updateAvailableCommandsAndNotifyIfChanged(); listeners.flushEvents(); @@ -1032,6 +1044,7 @@ public final class CastPlayer extends BasePlayer { boolean hasChanged = !currentTrackGroups.isEmpty(); currentTrackGroups = TrackGroupArray.EMPTY; currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY; + currentTracksInfo = TracksInfo.EMPTY; return hasChanged; } long[] activeTrackIds = mediaStatus.getActiveTrackIds(); @@ -1040,7 +1053,9 @@ public final class CastPlayer extends BasePlayer { } TrackGroup[] trackGroups = new TrackGroup[castMediaTracks.size()]; - TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT]; + @NullableType TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT]; + TracksInfo.TrackGroupInfo[] trackGroupInfos = + new TracksInfo.TrackGroupInfo[castMediaTracks.size()]; for (int i = 0; i < castMediaTracks.size(); i++) { MediaTrack mediaTrack = castMediaTracks.get(i); trackGroups[i] = new TrackGroup(CastUtils.mediaTrackToFormat(mediaTrack)); @@ -1048,19 +1063,28 @@ public final class CastPlayer extends BasePlayer { long id = mediaTrack.getId(); @C.TrackType int trackType = MimeTypes.getTrackType(mediaTrack.getContentType()); int rendererIndex = getRendererIndexForTrackType(trackType); - if (isTrackActive(id, activeTrackIds) - && rendererIndex != C.INDEX_UNSET - && trackSelections[rendererIndex] == null) { + boolean supported = rendererIndex != C.INDEX_UNSET; + boolean selected = + isTrackActive(id, activeTrackIds) && supported && trackSelections[rendererIndex] == null; + if (selected) { trackSelections[rendererIndex] = new CastTrackSelection(trackGroups[i]); } + @C.FormatSupport + int[] trackSupport = new int[] {supported ? C.FORMAT_HANDLED : C.FORMAT_UNSUPPORTED_TYPE}; + final boolean[] trackSelected = new boolean[] {selected}; + trackGroupInfos[i] = + new TracksInfo.TrackGroupInfo(trackGroups[i], trackSupport, trackType, trackSelected); } TrackGroupArray newTrackGroups = new TrackGroupArray(trackGroups); TrackSelectionArray newTrackSelections = new TrackSelectionArray(trackSelections); + TracksInfo newTracksInfo = new TracksInfo(ImmutableList.copyOf(trackGroupInfos)); if (!newTrackGroups.equals(currentTrackGroups) - || !newTrackSelections.equals(currentTrackSelection)) { - currentTrackSelection = new TrackSelectionArray(trackSelections); - currentTrackGroups = new TrackGroupArray(trackGroups); + || !newTrackSelections.equals(currentTrackSelection) + || !newTracksInfo.equals(currentTracksInfo)) { + currentTrackSelection = newTrackSelections; + currentTrackGroups = newTrackGroups; + currentTracksInfo = newTracksInfo; return true; } return false; diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java index 66612a17a2..a958449b2a 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/FakePlayer.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.TracksInfo; import com.google.android.exoplayer2.testutil.StubExoPlayer; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; @@ -240,6 +241,11 @@ import com.google.android.exoplayer2.util.ListenerSet; return new TrackSelectionArray(); } + @Override + public TracksInfo getCurrentTracksInfo() { + return TracksInfo.EMPTY; + } + @Override public TrackSelectionParameters getTrackSelectionParameters() { return TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT; diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ForwardingPlayer.java b/library/common/src/main/java/com/google/android/exoplayer2/ForwardingPlayer.java index 4fa547d2dc..c45c48495b 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/ForwardingPlayer.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/ForwardingPlayer.java @@ -360,6 +360,11 @@ public class ForwardingPlayer implements Player { return player.getCurrentTrackSelections(); } + @Override + public TracksInfo getCurrentTracksInfo() { + return player.getCurrentTracksInfo(); + } + @Override public TrackSelectionParameters getTrackSelectionParameters() { return player.getTrackSelectionParameters(); @@ -645,6 +650,11 @@ public class ForwardingPlayer implements Player { eventListener.onTracksChanged(trackGroups, trackSelections); } + @Override + public void onTracksInfoChanged(TracksInfo tracksInfo) { + eventListener.onTracksInfoChanged(tracksInfo); + } + @Override public void onMediaMetadataChanged(MediaMetadata mediaMetadata) { eventListener.onMediaMetadataChanged(mediaMetadata); diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Player.java b/library/common/src/main/java/com/google/android/exoplayer2/Player.java index 367194350b..96f6e12487 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Player.java @@ -57,11 +57,8 @@ import java.util.List; * */ public interface Player { @@ -122,10 +119,22 @@ public interface Player { * concrete implementation may include null elements if it has a fixed number of renderer * components, wishes to report a TrackSelection for each of them, and has one or more * renderer components that is not assigned any selected tracks. + * @deprecated Use {@link #onTracksInfoChanged(TracksInfo)} instead. */ + @Deprecated default void onTracksChanged( TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {} + /** + * Called when the available or selected tracks change. + * + *

{@link #onEvents(Player, Events)} will also be called to report this event along with + * other events that happen in the same {@link Looper} message queue iteration. + * + * @param tracksInfo The available tracks information. Never null, but may be of length zero. + */ + default void onTracksInfoChanged(TracksInfo tracksInfo) {} + /** * Called when the combined {@link MediaMetadata} changes. * @@ -693,6 +702,7 @@ public interface Player { COMMAND_SET_VIDEO_SURFACE, COMMAND_GET_TEXT, COMMAND_SET_TRACK_SELECTION_PARAMETERS, + COMMAND_GET_TRACK_INFOS, }; private final FlagSet.Builder flagsBuilder; @@ -923,8 +933,7 @@ public interface Player { @Nullable MediaItem mediaItem, @MediaItemTransitionReason int reason) {} @Override - default void onTracksChanged( - TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {} + default void onTracksInfoChanged(TracksInfo tracksInfo) {} @Override default void onIsLoadingChanged(boolean isLoading) {} @@ -1278,7 +1287,7 @@ public interface Player { int EVENT_TIMELINE_CHANGED = 0; /** {@link #getCurrentMediaItem()} changed or the player started repeating the current item. */ int EVENT_MEDIA_ITEM_TRANSITION = 1; - /** {@link #getCurrentTrackGroups()} or {@link #getCurrentTrackSelections()} changed. */ + /** {@link #getCurrentTracksInfo()} changed. */ int EVENT_TRACKS_CHANGED = 2; /** {@link #isLoading()} ()} changed. */ int EVENT_IS_LOADING_CHANGED = 3; @@ -1331,8 +1340,8 @@ public interface Player { * #COMMAND_CHANGE_MEDIA_ITEMS}, {@link #COMMAND_GET_AUDIO_ATTRIBUTES}, {@link * #COMMAND_GET_VOLUME}, {@link #COMMAND_GET_DEVICE_VOLUME}, {@link #COMMAND_SET_VOLUME}, {@link * #COMMAND_SET_DEVICE_VOLUME}, {@link #COMMAND_ADJUST_DEVICE_VOLUME}, {@link - * #COMMAND_SET_VIDEO_SURFACE}, {@link #COMMAND_GET_TEXT} or {@link - * #COMMAND_SET_TRACK_SELECTION_PARAMETERS}. + * #COMMAND_SET_VIDEO_SURFACE}, {@link #COMMAND_GET_TEXT}, {@link + * #COMMAND_SET_TRACK_SELECTION_PARAMETERS} or {@link #COMMAND_GET_TRACK_INFOS}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -1366,6 +1375,7 @@ public interface Player { COMMAND_SET_VIDEO_SURFACE, COMMAND_GET_TEXT, COMMAND_SET_TRACK_SELECTION_PARAMETERS, + COMMAND_GET_TRACK_INFOS, }) @interface Command {} /** Command to start, pause or resume playback. */ @@ -1424,6 +1434,8 @@ public interface Player { int COMMAND_GET_TEXT = 27; /** Command to set the player's track selection parameters. */ int COMMAND_SET_TRACK_SELECTION_PARAMETERS = 28; + /** Command to get track infos. */ + int COMMAND_GET_TRACK_INFOS = 29; /** Represents an invalid {@link Command}. */ int COMMAND_INVALID = -1; @@ -1962,7 +1974,9 @@ public interface Player { * Returns the available track groups. * * @see Listener#onTracksChanged(TrackGroupArray, TrackSelectionArray) + * @deprecated Use {@link #getCurrentTracksInfo()}. */ + @Deprecated TrackGroupArray getCurrentTrackGroups(); /** @@ -1973,10 +1987,23 @@ public interface Player { * components that is not assigned any selected tracks. * * @see Listener#onTracksChanged(TrackGroupArray, TrackSelectionArray) + * @deprecated Use {@link #getCurrentTracksInfo()}. */ + @Deprecated TrackSelectionArray getCurrentTrackSelections(); - /** Returns the parameters constraining the track selection. */ + /** + * Returns the available tracks, as well as the tracks' support, type, and selection status. + * + * @see Listener#onTracksChanged(TrackGroupArray, TrackSelectionArray) + */ + TracksInfo getCurrentTracksInfo(); + + /** + * Returns the parameters constraining the track selection. + * + * @see Listener#onTrackSelectionParametersChanged} + */ TrackSelectionParameters getTrackSelectionParameters(); /** diff --git a/library/common/src/main/java/com/google/android/exoplayer2/TracksInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/TracksInfo.java new file mode 100644 index 0000000000..3f587ed8d0 --- /dev/null +++ b/library/common/src/main/java/com/google/android/exoplayer2/TracksInfo.java @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2; + +import static com.google.android.exoplayer2.util.Assertions.checkArgument; +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.fromNullableBundle; +import static com.google.android.exoplayer2.util.BundleableUtil.toBundleArrayList; + +import android.os.Bundle; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Booleans; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.List; + +/** Immutable information ({@link TrackGroupInfo}) about tracks. */ +public final class TracksInfo implements Bundleable { + /** + * Information about tracks in a {@link TrackGroup}: their {@link C.TrackType}, if their format is + * supported by the player and if they are selected for playback. + */ + public static final class TrackGroupInfo implements Bundleable { + private final TrackGroup trackGroup; + @C.FormatSupport private final int[] trackSupport; + private final @C.TrackType int trackType; + private final boolean[] trackSelected; + + /** + * Constructs a TrackGroupInfo. + * + * @param trackGroup The {@link TrackGroup} described. + * @param trackSupport The {@link C.FormatSupport} of each track in the {@code trackGroup}. + * @param trackType The {@link C.TrackType} of the tracks in the {@code trackGroup}. + * @param tracksSelected Whether a track is selected for each track in {@code trackGroup}. + */ + public TrackGroupInfo( + TrackGroup trackGroup, + @C.FormatSupport int[] trackSupport, + @C.TrackType int trackType, + boolean[] tracksSelected) { + int length = trackGroup.length; + checkArgument(length == trackSupport.length && length == tracksSelected.length); + this.trackGroup = trackGroup; + this.trackSupport = trackSupport.clone(); + this.trackType = trackType; + this.trackSelected = tracksSelected.clone(); + } + + /** Returns the {@link TrackGroup} described by this {@code TrackGroupInfo}. */ + public TrackGroup getTrackGroup() { + return trackGroup; + } + + /** + * Returns the level of support for a track in a {@link TrackGroup}. + * + * @param trackIndex The index of the track in the {@link TrackGroup}. + * @return The {@link C.FormatSupport} of the track. + */ + @C.FormatSupport + public int getTrackSupport(int trackIndex) { + return trackSupport[trackIndex]; + } + + /** + * Returns if a track in a {@link TrackGroup} is supported for playback. + * + * @param trackIndex The index of the track in the {@link TrackGroup}. + * @return True if the track's format can be played, false otherwise. + */ + public boolean isTrackSupported(int trackIndex) { + return trackSupport[trackIndex] == C.FORMAT_HANDLED; + } + + /** Returns if at least one track in a {@link TrackGroup} is selected for playback. */ + public boolean isSelected() { + return Booleans.contains(trackSelected, true); + } + + /** Returns if at least one track in a {@link TrackGroup} is supported. */ + public boolean isSupported() { + for (int i = 0; i < trackSupport.length; i++) { + if (isTrackSupported(i)) { + return true; + } + } + return false; + } + + /** + * Returns if a track in a {@link TrackGroup} is selected for playback. + * + *

Multiple tracks of a track group may be selected, in which case the the active one + * (currently playing) is undefined. This is common in adaptive streaming, where multiple tracks + * of different quality are selected and the active one changes depending on the network and the + * {@link TrackSelectionParameters}. + * + * @param trackIndex The index of the track in the {@link TrackGroup}. + * @return true if the track is selected, false otherwise. + */ + public boolean isTrackSelected(int trackIndex) { + return trackSelected[trackIndex]; + } + + /** + * Returns the {@link C.TrackType} of the tracks in the {@link TrackGroup}. Tracks in a group + * are all of the same type. + */ + public @C.TrackType int getTrackType() { + return trackType; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + TrackGroupInfo that = (TrackGroupInfo) other; + return trackType == that.trackType + && trackGroup.equals(that.trackGroup) + && Arrays.equals(trackSupport, that.trackSupport) + && Arrays.equals(trackSelected, that.trackSelected); + } + + @Override + public int hashCode() { + int result = trackGroup.hashCode(); + result = 31 * result + Arrays.hashCode(trackSupport); + result = 31 * result + trackType; + result = 31 * result + Arrays.hashCode(trackSelected); + return result; + } + + // Bundleable implementation. + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + FIELD_TRACK_GROUP, + FIELD_TRACK_SUPPORT, + FIELD_TRACK_TYPE, + FIELD_TRACK_SELECTED, + }) + private @interface FieldNumber {} + + private static final int FIELD_TRACK_GROUP = 0; + private static final int FIELD_TRACK_SUPPORT = 1; + private static final int FIELD_TRACK_TYPE = 2; + private static final int FIELD_TRACK_SELECTED = 3; + + @Override + public Bundle toBundle() { + Bundle bundle = new Bundle(); + bundle.putBundle(keyForField(FIELD_TRACK_GROUP), trackGroup.toBundle()); + bundle.putIntArray(keyForField(FIELD_TRACK_SUPPORT), trackSupport); + bundle.putInt(keyForField(FIELD_TRACK_TYPE), trackType); + bundle.putBooleanArray(keyForField(FIELD_TRACK_SELECTED), trackSelected); + return bundle; + } + + /** Object that can restores a {@code TracksInfo} from a {@link Bundle}. */ + public static final Creator CREATOR = + bundle -> { + TrackGroup trackGroup = + fromNullableBundle( + TrackGroup.CREATOR, bundle.getBundle(keyForField(FIELD_TRACK_GROUP))); + checkNotNull(trackGroup); // Can't create a trackGroup info without a trackGroup + @C.FormatSupport + final int[] trackSupport = + MoreObjects.firstNonNull( + bundle.getIntArray(keyForField(FIELD_TRACK_SUPPORT)), new int[trackGroup.length]); + @C.TrackType + int trackType = bundle.getInt(keyForField(FIELD_TRACK_TYPE), C.TRACK_TYPE_UNKNOWN); + boolean[] selected = + MoreObjects.firstNonNull( + bundle.getBooleanArray(keyForField(FIELD_TRACK_SELECTED)), + new boolean[trackGroup.length]); + return new TrackGroupInfo(trackGroup, trackSupport, trackType, selected); + }; + + private static String keyForField(@FieldNumber int field) { + return Integer.toString(field, Character.MAX_RADIX); + } + } + + private final ImmutableList trackGroupInfos; + + /** An empty {@code TrackInfo} containing no {@link TrackGroupInfo}. */ + public static final TracksInfo EMPTY = new TracksInfo(ImmutableList.of()); + + /** Constructs {@code TracksInfo} from the provided {@link TrackGroupInfo}. */ + public TracksInfo(List trackGroupInfos) { + this.trackGroupInfos = ImmutableList.copyOf(trackGroupInfos); + } + + /** Returns the {@link TrackGroupInfo TrackGroupInfos}, describing each {@link TrackGroup}. */ + public ImmutableList getTrackGroupInfos() { + return trackGroupInfos; + } + + /** Returns if there is at least one track of type {@code trackType} but none are supported. */ + public boolean isTypeSupportedOrEmpty(@C.TrackType int trackType) { + boolean supported = true; + for (int i = 0; i < trackGroupInfos.size(); i++) { + if (trackGroupInfos.get(i).trackType == trackType) { + if (trackGroupInfos.get(i).isSupported()) { + return true; + } else { + supported = false; + } + } + } + return supported; + } + + /** Returns if at least one track of the type {@code trackType} is selected for playback. */ + public boolean isTypeSelected(@C.TrackType int trackType) { + for (int i = 0; i < trackGroupInfos.size(); i++) { + TrackGroupInfo trackGroupInfo = trackGroupInfos.get(i); + if (trackGroupInfo.isSelected() && trackGroupInfo.getTrackType() == trackType) { + return true; + } + } + return false; + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + TracksInfo that = (TracksInfo) other; + return trackGroupInfos.equals(that.trackGroupInfos); + } + + @Override + public int hashCode() { + return trackGroupInfos.hashCode(); + } + // Bundleable implementation. + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + FIELD_TRACK_GROUP_INFOS, + }) + private @interface FieldNumber {} + + private static final int FIELD_TRACK_GROUP_INFOS = 0; + + @Override + public Bundle toBundle() { + Bundle bundle = new Bundle(); + bundle.putParcelableArrayList( + keyForField(FIELD_TRACK_GROUP_INFOS), toBundleArrayList(trackGroupInfos)); + return bundle; + } + + /** Object that can restore a {@code TracksInfo} from a {@link Bundle}. */ + public static final Creator CREATOR = + bundle -> { + List trackGroupInfos = + fromBundleNullableList( + TrackGroupInfo.CREATOR, + bundle.getParcelableArrayList(keyForField(FIELD_TRACK_GROUP_INFOS)), + /* defaultValue= */ ImmutableList.of()); + return new TracksInfo(trackGroupInfos); + }; + + private static String keyForField(@FieldNumber int field) { + return Integer.toString(field, Character.MAX_RADIX); + } +} diff --git a/library/common/src/test/java/com/google/android/exoplayer2/TracksInfoTest.java b/library/common/src/test/java/com/google/android/exoplayer2/TracksInfoTest.java new file mode 100644 index 0000000000..3e285c9b35 --- /dev/null +++ b/library/common/src/test/java/com/google/android/exoplayer2/TracksInfoTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link TracksInfo}. */ +@RunWith(AndroidJUnit4.class) +public class TracksInfoTest { + + @Test + public void roundTripViaBundle_ofEmptyTracksInfo_yieldsEqualInstance() { + TracksInfo before = TracksInfo.EMPTY; + TracksInfo after = TracksInfo.CREATOR.fromBundle(before.toBundle()); + assertThat(after).isEqualTo(before); + } + + @Test + public void roundTripViaBundle_ofTracksInfo_yieldsEqualInstance() { + TracksInfo before = + new TracksInfo( + ImmutableList.of( + new TracksInfo.TrackGroupInfo( + new TrackGroup(new Format.Builder().build()), + new int[] {C.FORMAT_EXCEEDS_CAPABILITIES}, + C.TRACK_TYPE_AUDIO, + new boolean[] {true}), + new TracksInfo.TrackGroupInfo( + new TrackGroup(new Format.Builder().build(), new Format.Builder().build()), + new int[] {C.FORMAT_UNSUPPORTED_DRM, C.FORMAT_UNSUPPORTED_TYPE}, + C.TRACK_TYPE_VIDEO, + new boolean[] {false, true}))); + TracksInfo after = TracksInfo.CREATOR.fromBundle(before.toBundle()); + assertThat(after).isEqualTo(before); + } + + @Test + public void tracksInfoGetters_withoutTrack_returnExpectedValues() { + TracksInfo tracksInfo = new TracksInfo(ImmutableList.of()); + + assertThat(tracksInfo.isTypeSupportedOrEmpty(C.TRACK_TYPE_AUDIO)).isTrue(); + assertThat(tracksInfo.isTypeSelected(C.TRACK_TYPE_AUDIO)).isFalse(); + ImmutableList trackGroupInfos = tracksInfo.getTrackGroupInfos(); + assertThat(trackGroupInfos).isEmpty(); + } + + @Test + public void tracksInfo_emptyStaticInstance_isEmpty() { + TracksInfo tracksInfo = TracksInfo.EMPTY; + + assertThat(tracksInfo.getTrackGroupInfos()).isEmpty(); + assertThat(tracksInfo).isEqualTo(new TracksInfo(ImmutableList.of())); + } + + @Test + public void tracksInfoGetters_ofComplexTracksInfo_returnExpectedValues() { + TracksInfo.TrackGroupInfo trackGroupInfo0 = + new TracksInfo.TrackGroupInfo( + new TrackGroup(new Format.Builder().build()), + new int[] {C.FORMAT_EXCEEDS_CAPABILITIES}, + C.TRACK_TYPE_AUDIO, + /* tracksSelected= */ new boolean[] {false}); + TracksInfo.TrackGroupInfo trackGroupInfo1 = + new TracksInfo.TrackGroupInfo( + new TrackGroup(new Format.Builder().build(), new Format.Builder().build()), + new int[] {C.FORMAT_UNSUPPORTED_DRM, C.FORMAT_HANDLED}, + C.TRACK_TYPE_VIDEO, + /* tracksSelected= */ new boolean[] {false, true}); + TracksInfo tracksInfo = new TracksInfo(ImmutableList.of(trackGroupInfo0, trackGroupInfo1)); + + assertThat(tracksInfo.isTypeSupportedOrEmpty(C.TRACK_TYPE_AUDIO)).isFalse(); + assertThat(tracksInfo.isTypeSupportedOrEmpty(C.TRACK_TYPE_VIDEO)).isTrue(); + assertThat(tracksInfo.isTypeSupportedOrEmpty(C.TRACK_TYPE_TEXT)).isTrue(); + assertThat(tracksInfo.isTypeSelected(C.TRACK_TYPE_AUDIO)).isFalse(); + assertThat(tracksInfo.isTypeSelected(C.TRACK_TYPE_VIDEO)).isTrue(); + ImmutableList trackGroupInfos = tracksInfo.getTrackGroupInfos(); + assertThat(trackGroupInfos).hasSize(2); + assertThat(trackGroupInfos.get(0)).isSameInstanceAs(trackGroupInfo0); + assertThat(trackGroupInfos.get(1)).isSameInstanceAs(trackGroupInfo1); + assertThat(trackGroupInfos.get(0).isTrackSupported(0)).isFalse(); + assertThat(trackGroupInfos.get(1).isTrackSupported(0)).isFalse(); + assertThat(trackGroupInfos.get(1).isTrackSupported(1)).isTrue(); + assertThat(trackGroupInfos.get(0).getTrackSupport(0)).isEqualTo(C.FORMAT_EXCEEDS_CAPABILITIES); + assertThat(trackGroupInfos.get(1).getTrackSupport(0)).isEqualTo(C.FORMAT_UNSUPPORTED_DRM); + assertThat(trackGroupInfos.get(1).getTrackSupport(1)).isEqualTo(C.FORMAT_HANDLED); + assertThat(trackGroupInfos.get(0).isTrackSelected(0)).isFalse(); + assertThat(trackGroupInfos.get(1).isTrackSelected(0)).isFalse(); + assertThat(trackGroupInfos.get(1).isTrackSelected(1)).isTrue(); + assertThat(trackGroupInfos.get(0).getTrackType()).isEqualTo(C.TRACK_TYPE_AUDIO); + assertThat(trackGroupInfos.get(1).getTrackType()).isEqualTo(C.TRACK_TYPE_VIDEO); + } +} 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 c91d0b746a..d3d041ce93 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 @@ -196,6 +196,7 @@ import java.util.concurrent.CopyOnWriteArraySet; new TrackSelectorResult( new RendererConfiguration[renderers.length], new ExoTrackSelection[renderers.length], + TracksInfo.EMPTY, /* info= */ null); period = new Timeline.Period(); permanentAvailableCommands = @@ -210,7 +211,8 @@ import java.util.concurrent.CopyOnWriteArraySet; COMMAND_GET_TIMELINE, COMMAND_GET_MEDIA_ITEMS_METADATA, COMMAND_SET_MEDIA_ITEMS_METADATA, - COMMAND_CHANGE_MEDIA_ITEMS) + COMMAND_CHANGE_MEDIA_ITEMS, + COMMAND_GET_TRACK_INFOS) .addIf(COMMAND_SET_TRACK_SELECTION_PARAMETERS, trackSelector.isSetParametersSupported()) .addAll(additionalPermanentAvailableCommands) .build(); @@ -951,6 +953,11 @@ import java.util.concurrent.CopyOnWriteArraySet; return new TrackSelectionArray(playbackInfo.trackSelectorResult.selections); } + @Override + public TracksInfo getCurrentTracksInfo() { + return playbackInfo.trackSelectorResult.tracksInfo; + } + @Override public TrackSelectionParameters getTrackSelectionParameters() { return trackSelector.getParameters(); @@ -1274,6 +1281,9 @@ import java.util.concurrent.CopyOnWriteArraySet; listeners.queueEvent( Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(newPlaybackInfo.trackGroups, newSelection)); + listeners.queueEvent( + Player.EVENT_TRACKS_CHANGED, + listener -> listener.onTracksInfoChanged(newPlaybackInfo.trackSelectorResult.tracksInfo)); } if (metadataChanged) { final MediaMetadata finalMediaMetadata = mediaMetadata; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 00a5d66be0..eb5dc12060 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -1456,6 +1456,12 @@ public class SimpleExoPlayer extends BasePlayer return player.getCurrentTrackSelections(); } + @Override + public TracksInfo getCurrentTracksInfo() { + verifyApplicationThread(); + return player.getCurrentTracksInfo(); + } + @Override public TrackSelectionParameters getTrackSelectionParameters() { verifyApplicationThread(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index 1e908e41e4..4d1bcd457f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.TracksInfo; import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioRendererEventListener; @@ -586,6 +587,7 @@ public class AnalyticsCollector } @Override + @SuppressWarnings("deprecation") // Implementing and calling deprecate listener method public final void onTracksChanged( TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); @@ -595,6 +597,15 @@ public class AnalyticsCollector listener -> listener.onTracksChanged(eventTime, trackGroups, trackSelections)); } + @Override + public void onTracksInfoChanged(TracksInfo tracksInfo) { + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); + sendEvent( + eventTime, + AnalyticsListener.EVENT_TRACKS_CHANGED, + listener -> listener.onTracksInfoChanged(eventTime, tracksInfo)); + } + @SuppressWarnings("deprecation") // Calling deprecated listener method. @Override public final void onIsLoadingChanged(boolean isLoading) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java index 0fca47cf56..9887f397ea 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -36,6 +36,7 @@ import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.Player.TimelineChangeReason; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.TracksInfo; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.decoder.DecoderCounters; @@ -221,7 +222,8 @@ public interface AnalyticsListener { */ int EVENT_MEDIA_ITEM_TRANSITION = Player.EVENT_MEDIA_ITEM_TRANSITION; /** - * {@link Player#getCurrentTrackGroups()} or {@link Player#getCurrentTrackSelections()} changed. + * {@link Player#getCurrentTracksInfo()}, {@link Player#getCurrentTrackGroups()} or {@link + * Player#getCurrentTrackSelections()} changed. */ int EVENT_TRACKS_CHANGED = Player.EVENT_TRACKS_CHANGED; /** {@link Player#isLoading()} ()} changed. */ @@ -674,10 +676,20 @@ public interface AnalyticsListener { * @param eventTime The event time. * @param trackGroups The available tracks. May be empty. * @param trackSelections The track selections for each renderer. May contain null elements. + * @deprecated Use {@link #onTracksInfoChanged}. */ + @Deprecated default void onTracksChanged( EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {} + /** + * Called when the available or selected tracks change. + * + * @param eventTime The event time. + * @param tracksInfo The available tracks information. Never null, but may be of length zero. + */ + default void onTracksInfoChanged(EventTime eventTime, TracksInfo tracksInfo) {} + /** * Called when the combined {@link MediaMetadata} changes. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index c45a69f45f..0287bb39b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -21,6 +21,7 @@ import static java.lang.Math.min; import android.util.Pair; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C.FormatSupport; import com.google.android.exoplayer2.ExoPlaybackException; @@ -30,11 +31,13 @@ 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.TracksInfo; 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.MimeTypes; import com.google.android.exoplayer2.util.Util; +import com.google.common.collect.ImmutableList; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -106,7 +109,7 @@ public abstract class MappingTrackSelector extends TrackSelector { * renderer, track group and track (in that order). * @param unmappedTrackGroups {@link TrackGroup}s not mapped to any renderer. */ - @SuppressWarnings("deprecation") + @VisibleForTesting /* package */ MappedTrackInfo( String[] rendererNames, @C.TrackType int[] rendererTrackTypes, @@ -144,7 +147,7 @@ public abstract class MappingTrackSelector extends TrackSelector { * * @see Renderer#getTrackType() * @param rendererIndex The renderer index. - * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}. + * @return The {@link C.TrackType} of the renderer. */ public @C.TrackType int getRendererType(int rendererIndex) { return rendererTrackTypes[rendererIndex]; @@ -279,6 +282,7 @@ public abstract class MappingTrackSelector extends TrackSelector { String firstSampleMimeType = null; for (int i = 0; i < trackIndices.length; i++) { int trackIndex = trackIndices[i]; + @Nullable String sampleMimeType = rendererTrackGroups[rendererIndex].get(groupIndex).getFormat(trackIndex).sampleMimeType; if (handledTrackCount++ == 0) { @@ -406,7 +410,10 @@ public abstract class MappingTrackSelector extends TrackSelector { rendererMixedMimeTypeAdaptationSupports, periodId, timeline); - return new TrackSelectorResult(result.first, result.second, mappedTrackInfo); + + TracksInfo tracksInfo = buildTracksInfo(result.second, mappedTrackInfo); + + return new TrackSelectorResult(result.first, result.second, tracksInfo, mappedTrackInfo); } /** @@ -536,4 +543,49 @@ public abstract class MappingTrackSelector extends TrackSelector { } return mixedMimeTypeAdaptationSupport; } + + @VisibleForTesting + /* package */ static TracksInfo buildTracksInfo( + @NullableType TrackSelection[] selections, MappedTrackInfo mappedTrackInfo) { + ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for (int rendererIndex = 0; + rendererIndex < mappedTrackInfo.getRendererCount(); + rendererIndex++) { + TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); + @Nullable TrackSelection trackSelection = selections[rendererIndex]; + for (int groupIndex = 0; groupIndex < trackGroupArray.length; groupIndex++) { + TrackGroup trackGroup = trackGroupArray.get(groupIndex); + @C.FormatSupport int[] trackSupport = new int[trackGroup.length]; + boolean[] selected = new boolean[trackGroup.length]; + for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { + trackSupport[trackIndex] = + mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex); + // Suppressing reference equality warning because the track group stored in the track + // selection must point to the exact track group object to be considered part of it. + @SuppressWarnings("ReferenceEquality") + boolean isTrackSelected = + (trackSelection != null) + && (trackSelection.getTrackGroup() == trackGroup) + && (trackSelection.indexOf(trackIndex) != C.INDEX_UNSET); + selected[trackIndex] = isTrackSelected; + } + @C.TrackType int trackGroupType = mappedTrackInfo.getRendererType(rendererIndex); + builder.add( + new TracksInfo.TrackGroupInfo(trackGroup, trackSupport, trackGroupType, selected)); + } + } + TrackGroupArray unmappedTrackGroups = mappedTrackInfo.getUnmappedTrackGroups(); + for (int groupIndex = 0; groupIndex < unmappedTrackGroups.length; groupIndex++) { + TrackGroup trackGroup = unmappedTrackGroups.get(groupIndex); + @C.FormatSupport int[] trackSupport = new int[trackGroup.length]; + Arrays.fill(trackSupport, C.FORMAT_UNSUPPORTED_TYPE); + // A track group only contains tracks of the same type, thus only consider the first track. + @C.TrackType + int trackGroupType = MimeTypes.getTrackType(trackGroup.getFormat(0).sampleMimeType); + boolean[] selected = new boolean[trackGroup.length]; // Initialized to false. + builder.add( + new TracksInfo.TrackGroupInfo(trackGroup, trackSupport, trackGroupType, selected)); + } + return new TracksInfo(builder.build()); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java index e7f0caaedf..a7bef4324b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.trackselection; import androidx.annotation.Nullable; import com.google.android.exoplayer2.RendererConfiguration; +import com.google.android.exoplayer2.TracksInfo; import com.google.android.exoplayer2.util.Util; import org.checkerframework.checker.nullness.compatqual.NullableType; @@ -32,6 +33,8 @@ public final class TrackSelectorResult { public final @NullableType RendererConfiguration[] rendererConfigurations; /** A {@link ExoTrackSelection} array containing the track selection for each renderer. */ public final @NullableType ExoTrackSelection[] selections; + /** Describe the tracks and which one were selected. */ + public final TracksInfo tracksInfo; /** * An opaque object that will be returned to {@link TrackSelector#onSelectionActivated(Object)} * should the selections be activated. @@ -45,13 +48,34 @@ public final class TrackSelectorResult { * @param info An opaque object that will be returned to {@link * TrackSelector#onSelectionActivated(Object)} should the selection be activated. May be * {@code null}. + * @deprecated Use {@link #TrackSelectorResult(RendererConfiguration[], ExoTrackSelection[], + * TracksInfo, Object)}. */ + @Deprecated public TrackSelectorResult( @NullableType RendererConfiguration[] rendererConfigurations, @NullableType ExoTrackSelection[] selections, @Nullable Object info) { + this(rendererConfigurations, selections, TracksInfo.EMPTY, info); + } + + /** + * @param rendererConfigurations A {@link RendererConfiguration} for each renderer. A null entry + * indicates the corresponding renderer should be disabled. + * @param selections A {@link ExoTrackSelection} array containing the selection for each renderer. + * @param tracksInfo Description of the available tracks and which one were selected. + * @param info An opaque object that will be returned to {@link + * TrackSelector#onSelectionActivated(Object)} should the selection be activated. May be + * {@code null}. + */ + public TrackSelectorResult( + @NullableType RendererConfiguration[] rendererConfigurations, + @NullableType ExoTrackSelection[] selections, + TracksInfo tracksInfo, + @Nullable Object info) { this.rendererConfigurations = rendererConfigurations; this.selections = selections.clone(); + this.tracksInfo = tracksInfo; this.info = info; length = rendererConfigurations.length; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 105a90471b..1de7410140 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -23,6 +23,7 @@ import static com.google.android.exoplayer2.Player.COMMAND_GET_DEVICE_VOLUME; import static com.google.android.exoplayer2.Player.COMMAND_GET_MEDIA_ITEMS_METADATA; import static com.google.android.exoplayer2.Player.COMMAND_GET_TEXT; import static com.google.android.exoplayer2.Player.COMMAND_GET_TIMELINE; +import static com.google.android.exoplayer2.Player.COMMAND_GET_TRACK_INFOS; import static com.google.android.exoplayer2.Player.COMMAND_GET_VOLUME; import static com.google.android.exoplayer2.Player.COMMAND_PLAY_PAUSE; import static com.google.android.exoplayer2.Player.COMMAND_PREPARE_STOP; @@ -8344,6 +8345,7 @@ public final class ExoPlayerTest { assertThat(player.isCommandAvailable(COMMAND_SET_VIDEO_SURFACE)).isTrue(); assertThat(player.isCommandAvailable(COMMAND_GET_TEXT)).isTrue(); assertThat(player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)).isTrue(); + assertThat(player.isCommandAvailable(COMMAND_GET_TRACK_INFOS)).isTrue(); } @Test @@ -11238,7 +11240,8 @@ public final class ExoPlayerTest { COMMAND_ADJUST_DEVICE_VOLUME, COMMAND_SET_VIDEO_SURFACE, COMMAND_GET_TEXT, - COMMAND_SET_TRACK_SELECTION_PARAMETERS); + COMMAND_SET_TRACK_SELECTION_PARAMETERS, + COMMAND_GET_TRACK_INFOS); if (!isTimelineEmpty) { builder.add(COMMAND_SEEK_TO_PREVIOUS); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java index 58d0d0c47b..635167e658 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java @@ -771,7 +771,10 @@ public final class MediaPeriodQueueTest { mediaSourceList, getNextMediaPeriodInfo(), new TrackSelectorResult( - new RendererConfiguration[0], new ExoTrackSelection[0], /* info= */ null)); + new RendererConfiguration[0], + new ExoTrackSelection[0], + TracksInfo.EMPTY, + /* info= */ null)); } private void clear() { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index 0753464f04..4be75e1443 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -84,6 +84,7 @@ import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.TracksInfo; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmInitData; @@ -100,7 +101,6 @@ import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.testutil.ActionSchedule; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; @@ -115,7 +115,6 @@ import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinit import com.google.android.exoplayer2.testutil.FakeVideoRenderer; import com.google.android.exoplayer2.testutil.TestExoPlayerBuilder; import com.google.android.exoplayer2.testutil.TestUtil; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.MimeTypes; @@ -2224,8 +2223,7 @@ public final class AnalyticsCollectorTest { } @Override - public void onTracksChanged( - EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + public void onTracksInfoChanged(EventTime eventTime, TracksInfo tracksInfo) { reportedEvents.add(new ReportedEvent(EVENT_TRACKS_CHANGED, eventTime)); } 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 146e670798..e04a0530a7 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 @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.TracksInfo; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -50,6 +51,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationLi 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.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.util.HashMap; @@ -1717,6 +1719,32 @@ public final class DefaultTrackSelectorTest { assertFixedSelection(result.selections[0], trackGroups, formatAac); } + /** Tests audio track selection when there are multiple audio renderers. */ + @Test + public void selectTracks_multipleRenderer_allSelected() throws Exception { + RendererCapabilities[] rendererCapabilities = + new RendererCapabilities[] {VIDEO_CAPABILITIES, AUDIO_CAPABILITIES, AUDIO_CAPABILITIES}; + TrackGroupArray trackGroups = new TrackGroupArray(AUDIO_TRACK_GROUP); + + TrackSelectorResult result = + trackSelector.selectTracks(rendererCapabilities, trackGroups, periodId, TIMELINE); + + assertThat(result.length).isEqualTo(3); + assertThat(result.rendererConfigurations) + .asList() + .containsExactly(null, DEFAULT, null) + .inOrder(); + assertThat(result.selections[0]).isNull(); + assertFixedSelection(result.selections[1], trackGroups, trackGroups.get(0).getFormat(0)); + assertThat(result.selections[2]).isNull(); + ImmutableList trackGroupInfos = + result.tracksInfo.getTrackGroupInfos(); + assertThat(trackGroupInfos).hasSize(1); + assertThat(trackGroupInfos.get(0).getTrackGroup()).isEqualTo(AUDIO_TRACK_GROUP); + assertThat(trackGroupInfos.get(0).isTrackSelected(0)).isTrue(); + assertThat(trackGroupInfos.get(0).getTrackSupport(0)).isEqualTo(FORMAT_HANDLED); + } + private static void assertSelections(TrackSelectorResult result, TrackSelection[] expected) { assertThat(result.length).isEqualTo(expected.length); for (int i = 0; i < expected.length; i++) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java index 9a01f85aa9..ab1d3c14cc 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java @@ -27,12 +27,15 @@ 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.TracksInfo; +import com.google.android.exoplayer2.TracksInfo.TrackGroupInfo; 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.testutil.FakeTimeline; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; +import com.google.common.collect.ImmutableList; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; @@ -129,6 +132,61 @@ public final class MappingTrackSelectorTest { return new TrackGroup(new Format.Builder().setSampleMimeType(sampleMimeType).build()); } + @Test + public void buildTrackInfos_withTestValues_isAsExpected() { + MappingTrackSelector.MappedTrackInfo mappedTrackInfo = + new MappingTrackSelector.MappedTrackInfo( + new String[] {"1", "2"}, + new int[] {C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_VIDEO}, + new TrackGroupArray[] { + new TrackGroupArray( + new TrackGroup(new Format.Builder().build()), + new TrackGroup(new Format.Builder().build())), + new TrackGroupArray( + new TrackGroup(new Format.Builder().build(), new Format.Builder().build())) + }, + new int[] { + RendererCapabilities.ADAPTIVE_SEAMLESS, RendererCapabilities.ADAPTIVE_NOT_SUPPORTED + }, + new int[][][] { + new int[][] {new int[] {C.FORMAT_HANDLED}, new int[] {C.FORMAT_UNSUPPORTED_SUBTYPE}}, + new int[][] {new int[] {C.FORMAT_UNSUPPORTED_DRM, C.FORMAT_EXCEEDS_CAPABILITIES}} + }, + new TrackGroupArray(new TrackGroup(new Format.Builder().build()))); + TrackSelection[] selections = + new TrackSelection[] { + new FixedTrackSelection(mappedTrackInfo.getTrackGroups(0).get(1), 0), + new FixedTrackSelection(mappedTrackInfo.getTrackGroups(1).get(0), 1) + }; + + TracksInfo tracksInfo = MappingTrackSelector.buildTracksInfo(selections, mappedTrackInfo); + + ImmutableList trackGroupInfos = tracksInfo.getTrackGroupInfos(); + assertThat(trackGroupInfos).hasSize(4); + assertThat(trackGroupInfos.get(0).getTrackGroup()) + .isEqualTo(mappedTrackInfo.getTrackGroups(0).get(0)); + assertThat(trackGroupInfos.get(1).getTrackGroup()) + .isEqualTo(mappedTrackInfo.getTrackGroups(0).get(1)); + assertThat(trackGroupInfos.get(2).getTrackGroup()) + .isEqualTo(mappedTrackInfo.getTrackGroups(1).get(0)); + assertThat(trackGroupInfos.get(3).getTrackGroup()) + .isEqualTo(mappedTrackInfo.getUnmappedTrackGroups().get(0)); + assertThat(trackGroupInfos.get(0).getTrackSupport(0)).isEqualTo(C.FORMAT_HANDLED); + assertThat(trackGroupInfos.get(1).getTrackSupport(0)).isEqualTo(C.FORMAT_UNSUPPORTED_SUBTYPE); + assertThat(trackGroupInfos.get(2).getTrackSupport(0)).isEqualTo(C.FORMAT_UNSUPPORTED_DRM); + assertThat(trackGroupInfos.get(2).getTrackSupport(1)).isEqualTo(C.FORMAT_EXCEEDS_CAPABILITIES); + assertThat(trackGroupInfos.get(3).getTrackSupport(0)).isEqualTo(C.FORMAT_UNSUPPORTED_TYPE); + assertThat(trackGroupInfos.get(0).isTrackSelected(0)).isFalse(); + assertThat(trackGroupInfos.get(1).isTrackSelected(0)).isTrue(); + assertThat(trackGroupInfos.get(2).isTrackSelected(0)).isFalse(); + assertThat(trackGroupInfos.get(2).isTrackSelected(1)).isTrue(); + assertThat(trackGroupInfos.get(3).isTrackSelected(0)).isFalse(); + assertThat(trackGroupInfos.get(0).getTrackType()).isEqualTo(C.TRACK_TYPE_AUDIO); + assertThat(trackGroupInfos.get(1).getTrackType()).isEqualTo(C.TRACK_TYPE_AUDIO); + assertThat(trackGroupInfos.get(2).getTrackType()).isEqualTo(C.TRACK_TYPE_VIDEO); + assertThat(trackGroupInfos.get(3).getTrackType()).isEqualTo(C.TRACK_TYPE_UNKNOWN); + } + /** * A {@link MappingTrackSelector} that stashes the {@link MappedTrackInfo} passed to {@link * #selectTracks(MappedTrackInfo, int[][][], int[], MediaPeriodId, Timeline)}. diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java index 7e7ead9440..7c6ba909c9 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java @@ -45,6 +45,7 @@ import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.TracksInfo; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; @@ -53,10 +54,8 @@ import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceFactory; -import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -636,8 +635,7 @@ public final class Transformer { } @Override - public void onTracksChanged( - EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + public void onTracksInfoChanged(EventTime eventTime, TracksInfo tracksInfo) { if (muxerWrapper.getTrackCount() == 0) { handleTransformationEnded( new IllegalStateException( diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index d96170d4e2..53255ff69d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -56,7 +56,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.TracksInfo; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -1511,7 +1511,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { } @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray selections) { + public void onTracksInfoChanged(TracksInfo tracksInfo) { // Suppress the update if transitioning to an unprepared period within the same window. This // is necessary to avoid closing the shutter when such a transition occurs. See: // https://github.com/google/ExoPlayer/issues/5507. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java index 72534209dc..6a30814673 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java @@ -57,7 +57,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.TracksInfo; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -1551,7 +1551,7 @@ public class StyledPlayerView extends FrameLayout implements AdViewProvider { } @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray selections) { + public void onTracksInfoChanged(TracksInfo tracksInfo) { // Suppress the update if transitioning to an unprepared period within the same window. This // is necessary to avoid closing the shutter when such a transition occurs. See: // https://github.com/google/ExoPlayer/issues/5507. diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index d3cd7fccaf..4096548a5c 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -38,9 +38,7 @@ import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.HandlerWrapper; @@ -383,7 +381,6 @@ public final class ExoPlayerTestRunner implements Player.Listener, ActionSchedul private SimpleExoPlayer player; private Exception exception; - private TrackGroupArray trackGroups; private boolean playerWasPrepared; private ExoPlayerTestRunner( @@ -562,17 +559,6 @@ public final class ExoPlayerTestRunner implements Player.Listener, ActionSchedul assertThat(playbackStates).containsExactlyElementsIn(states).inOrder(); } - /** - * Asserts that the last track group array reported by {@link - * Player.Listener#onTracksChanged(TrackGroupArray, TrackSelectionArray)} is equal to the provided - * track group array. - * - * @param trackGroupArray The expected {@link TrackGroupArray}. - */ - public void assertTrackGroupsEqual(TrackGroupArray trackGroupArray) { - assertThat(this.trackGroups).isEqualTo(trackGroupArray); - } - /** * Asserts that {@link Player.Listener#onPositionDiscontinuity(Player.PositionInfo, * Player.PositionInfo, int)} was not called. @@ -656,11 +642,6 @@ public final class ExoPlayerTestRunner implements Player.Listener, ActionSchedul mediaItemTransitionReasons.add(reason); } - @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - this.trackGroups = trackGroups; - } - @Override public void onPlaybackStateChanged(@Player.State int playbackState) { playbackStates.add(playbackState); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 72efc2b81b..db307b52db 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.TracksInfo; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AuxEffectInfo; import com.google.android.exoplayer2.source.MediaSource; @@ -463,6 +464,11 @@ public class StubExoPlayer extends BasePlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public TracksInfo getCurrentTracksInfo() { + throw new UnsupportedOperationException(); + } + @Override public TrackSelectionParameters getTrackSelectionParameters() { throw new UnsupportedOperationException();