/*
* Copyright 2019 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 androidx.media2.player;
import static androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_AUDIO;
import static androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_METADATA;
import static androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE;
import static androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_UNKNOWN;
import static androidx.media2.common.SessionPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO;
import static androidx.media2.player.RenderersFactory.AUDIO_RENDERER_INDEX;
import static androidx.media2.player.RenderersFactory.METADATA_RENDERER_INDEX;
import static androidx.media2.player.RenderersFactory.TEXT_RENDERER_INDEX;
import static androidx.media2.player.RenderersFactory.VIDEO_RENDERER_INDEX;
import static androidx.media2.player.TextRenderer.TRACK_TYPE_CEA608;
import static androidx.media2.player.TextRenderer.TRACK_TYPE_CEA708;
import static androidx.media2.player.TextRenderer.TRACK_TYPE_WEBVTT;
import static androidx.media2.player.TrackSelector.InternalTextTrackInfo.UNSET;
import android.annotation.SuppressLint;
import android.media.MediaFormat;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import androidx.core.util.Preconditions;
import androidx.media2.common.MediaItem;
import androidx.media2.common.SessionPlayer.TrackInfo;
import androidx.media2.exoplayer.external.C;
import androidx.media2.exoplayer.external.Format;
import androidx.media2.exoplayer.external.source.TrackGroup;
import androidx.media2.exoplayer.external.source.TrackGroupArray;
import androidx.media2.exoplayer.external.trackselection.DefaultTrackSelector;
import androidx.media2.exoplayer.external.trackselection.MappingTrackSelector;
import androidx.media2.exoplayer.external.trackselection.TrackSelection;
import androidx.media2.exoplayer.external.trackselection.TrackSelectionArray;
import androidx.media2.exoplayer.external.util.MimeTypes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Manages track selection for {@link ExoPlayerWrapper}.
*/
@SuppressLint("RestrictedApi") // TODO(b/68398926): Remove once RestrictedApi checks are fixed.
/* package */ final class TrackSelector {
private static final int TRACK_INDEX_UNSET = -1;
private int mNextTrackId;
private MediaItem mCurrentMediaItem;
private final TextRenderer mTextRenderer;
private final DefaultTrackSelector mDefaultTrackSelector;
private final SparseArray<InternalTrackInfo> mAudioTracks;
private final SparseArray<InternalTrackInfo> mVideoTracks;
private final SparseArray<InternalTrackInfo> mMetadataTracks;
private final SparseArray<InternalTextTrackInfo> mTextTracks;
private boolean mPendingTracksUpdate;
private InternalTrackInfo mSelectedAudioTrack;
private InternalTrackInfo mSelectedVideoTrack;
private InternalTrackInfo mSelectedMetadataTrack;
private InternalTextTrackInfo mSelectedTextTrack;
private int mPlayerTextTrackIndex;
TrackSelector(TextRenderer textRenderer) {
mTextRenderer = textRenderer;
mDefaultTrackSelector = new DefaultTrackSelector();
mAudioTracks = new SparseArray<>();
mVideoTracks = new SparseArray<>();
mMetadataTracks = new SparseArray<>();
mTextTracks = new SparseArray<>();
mSelectedAudioTrack = null;
mSelectedVideoTrack = null;
mSelectedMetadataTrack = null;
mSelectedTextTrack = null;
mPlayerTextTrackIndex = TRACK_INDEX_UNSET;
// Ensure undetermined text tracks are selected so that CEA-608/708 streams are sent to the
// text renderer. By default, metadata tracks are not selected.
mDefaultTrackSelector.setParameters(
new DefaultTrackSelector.ParametersBuilder()
.setSelectUndeterminedTextLanguage(true)
.setRendererDisabled(METADATA_RENDERER_INDEX, /* disabled= */ true));
}
public DefaultTrackSelector getPlayerTrackSelector() {
return mDefaultTrackSelector;
}
public void handlePlayerTracksChanged(MediaItem item, TrackSelectionArray trackSelections) {
final boolean itemChanged = mCurrentMediaItem != item;
mCurrentMediaItem = item;
mPendingTracksUpdate = true;
// Clear all selection state.
mDefaultTrackSelector.setParameters(
mDefaultTrackSelector.buildUponParameters().clearSelectionOverrides());
mSelectedAudioTrack = null;
mSelectedVideoTrack = null;
mSelectedMetadataTrack = null;
mSelectedTextTrack = null;
mPlayerTextTrackIndex = TRACK_INDEX_UNSET;
mTextRenderer.clearSelection();
if (itemChanged) {
mAudioTracks.clear();
mVideoTracks.clear();
mMetadataTracks.clear();
mTextTracks.clear();
}
MappingTrackSelector.MappedTrackInfo mappedTrackInfo =
mDefaultTrackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo == null) {
return;
}
// Get track selections to determine selected track.
TrackSelection audioTrackSelection = trackSelections.get(AUDIO_RENDERER_INDEX);
TrackGroup selectedAudioTrackGroup = audioTrackSelection == null ? null
: audioTrackSelection.getTrackGroup();
TrackSelection videoTrackSelection = trackSelections.get(VIDEO_RENDERER_INDEX);
TrackGroup selectedVideoTrackGroup = videoTrackSelection == null ? null
: videoTrackSelection.getTrackGroup();
TrackSelection metadataTrackSelection = trackSelections.get(METADATA_RENDERER_INDEX);
TrackGroup selectedMetadataTrackGroup = metadataTrackSelection == null ? null
: metadataTrackSelection.getTrackGroup();
TrackSelection textTrackSelection = trackSelections.get(TEXT_RENDERER_INDEX);
TrackGroup selectedTextTrackGroup = textTrackSelection == null ? null
: textTrackSelection.getTrackGroup();
// Enumerate track information.
TrackGroupArray audioTrackGroups = mappedTrackInfo.getTrackGroups(AUDIO_RENDERER_INDEX);
for (int i = mAudioTracks.size(); i < audioTrackGroups.length; i++) {
TrackGroup trackGroup = audioTrackGroups.get(i);
InternalTrackInfo track = new InternalTrackInfo(
i,
MEDIA_TRACK_TYPE_AUDIO,
ExoPlayerUtils.getMediaFormat(trackGroup.getFormat(0)),
mNextTrackId++);
mAudioTracks.put(track.mExternalTrackInfo.getId(), track);
if (trackGroup.equals(selectedAudioTrackGroup)) {
mSelectedAudioTrack = track;
}
}
TrackGroupArray videoTrackGroups = mappedTrackInfo.getTrackGroups(VIDEO_RENDERER_INDEX);
for (int i = mVideoTracks.size(); i < videoTrackGroups.length; i++) {
TrackGroup trackGroup = videoTrackGroups.get(i);
InternalTrackInfo track = new InternalTrackInfo(
i,
MEDIA_TRACK_TYPE_VIDEO,
ExoPlayerUtils.getMediaFormat(trackGroup.getFormat(0)),
mNextTrackId++);
mVideoTracks.put(track.mExternalTrackInfo.getId(), track);
if (trackGroup.equals(selectedVideoTrackGroup)) {
mSelectedVideoTrack = track;
}
}
TrackGroupArray metadataTrackGroups =
mappedTrackInfo.getTrackGroups(METADATA_RENDERER_INDEX);
for (int i = mMetadataTracks.size(); i < metadataTrackGroups.length; i++) {
TrackGroup trackGroup = metadataTrackGroups.get(i);
InternalTrackInfo track = new InternalTrackInfo(
i,
MEDIA_TRACK_TYPE_METADATA,
ExoPlayerUtils.getMediaFormat(trackGroup.getFormat(0)),
mNextTrackId++);
mMetadataTracks.put(track.mExternalTrackInfo.getId(), track);
if (trackGroup.equals(selectedMetadataTrackGroup)) {
mSelectedMetadataTrack = track;
}
}
// The text renderer exposes information about text tracks, but we may have preliminary
// information from the player.
TrackGroupArray textTrackGroups = mappedTrackInfo.getTrackGroups(TEXT_RENDERER_INDEX);
for (int i = mTextTracks.size(); i < textTrackGroups.length; i++) {
TrackGroup trackGroup = textTrackGroups.get(i);
Format format = Preconditions.checkNotNull(trackGroup.getFormat(0));
int type = getTextTrackType(format.sampleMimeType);
InternalTextTrackInfo textTrack = new InternalTextTrackInfo(
i, type, format, UNSET, mNextTrackId++);
mTextTracks.put(textTrack.mExternalTrackInfo.getId(), textTrack);
if (trackGroup.equals(selectedTextTrackGroup)) {
mPlayerTextTrackIndex = i;
}
}
}
public void handleTextRendererChannelAvailable(int type, int channel) {
// We may already be advertising a track for this type. If so, associate the existing text
// track with the channel. Otherwise create a new text track info.
boolean populatedExistingTrack = false;
for (int i = 0; i < mTextTracks.size(); i++) {
InternalTextTrackInfo textTrack = mTextTracks.valueAt(i);
if (textTrack.mType == type && textTrack.mChannel == UNSET) {
int trackId = textTrack.mExternalTrackInfo.getId();
// Associate the existing text track with this channel.
InternalTextTrackInfo replacementTextTrack = new InternalTextTrackInfo(
textTrack.mPlayerTrackIndex,
type,
textTrack.mFormat,
channel,
trackId);
mTextTracks.put(trackId, replacementTextTrack);
if (mSelectedTextTrack != null && mSelectedTextTrack.mPlayerTrackIndex == i) {
mTextRenderer.select(type, channel);
}
populatedExistingTrack = true;
break;
}
}
if (!populatedExistingTrack) {
InternalTextTrackInfo textTrack = new InternalTextTrackInfo(
mPlayerTextTrackIndex, type, /* format= */ null, channel, mNextTrackId++);
mTextTracks.put(textTrack.mExternalTrackInfo.getId(), textTrack);
mPendingTracksUpdate = true;
}
}
public boolean hasPendingTracksUpdate() {
boolean pendingTracksUpdate = mPendingTracksUpdate;
mPendingTracksUpdate = false;
return pendingTracksUpdate;
}
public TrackInfo getSelectedTrack(int trackType) {
switch (trackType) {
case MEDIA_TRACK_TYPE_AUDIO:
return mSelectedAudioTrack == null ? null
: mSelectedAudioTrack.mExternalTrackInfo;
case MEDIA_TRACK_TYPE_VIDEO:
return mSelectedVideoTrack == null ? null
: mSelectedVideoTrack.mExternalTrackInfo;
case MEDIA_TRACK_TYPE_METADATA:
return mSelectedMetadataTrack == null ? null
: mSelectedMetadataTrack.mExternalTrackInfo;
case MEDIA_TRACK_TYPE_SUBTITLE:
return mSelectedTextTrack == null ? null
: mSelectedTextTrack.mExternalTrackInfo;
case MEDIA_TRACK_TYPE_UNKNOWN:
default:
return null;
}
}
public List<TrackInfo> getTracks() {
ArrayList<TrackInfo> externalTracks = new ArrayList<>();
for (SparseArray<? extends InternalTrackInfo> tracks : Arrays.asList(
mAudioTracks, mVideoTracks, mMetadataTracks, mTextTracks)) {
for (int i = 0; i < tracks.size(); i++) {
externalTracks.add(tracks.valueAt(i).mExternalTrackInfo);
}
}
return externalTracks;
}
public void selectTrack(int trackId) {
InternalTrackInfo videoTrack = mVideoTracks.get(trackId);
Preconditions.checkArgument(videoTrack == null, "Video track selection is not supported");
InternalTrackInfo audioTrack = mAudioTracks.get(trackId);
if (audioTrack != null) {
mSelectedAudioTrack = audioTrack;
MappingTrackSelector.MappedTrackInfo mappedTrackInfo =
Preconditions.checkNotNull(mDefaultTrackSelector.getCurrentMappedTrackInfo());
TrackGroupArray audioTrackGroups = mappedTrackInfo.getTrackGroups(AUDIO_RENDERER_INDEX);
TrackGroup selectedTrackGroup = audioTrackGroups.get(audioTrack.mPlayerTrackIndex);
// Selected all adaptive tracks.
int[] trackIndices = new int[selectedTrackGroup.length];
for (int i = 0; i < trackIndices.length; i++) {
trackIndices[i] = i;
}
DefaultTrackSelector.SelectionOverride selectionOverride =
new DefaultTrackSelector.SelectionOverride(audioTrack.mPlayerTrackIndex,
trackIndices);
mDefaultTrackSelector.setParameters(mDefaultTrackSelector.buildUponParameters()
.setSelectionOverride(AUDIO_RENDERER_INDEX, audioTrackGroups, selectionOverride)
.build());
return;
}
InternalTrackInfo metadataTrack = mMetadataTracks.get(trackId);
if (metadataTrack != null) {
mSelectedMetadataTrack = metadataTrack;
MappingTrackSelector.MappedTrackInfo mappedTrackInfo =
Preconditions.checkNotNull(mDefaultTrackSelector.getCurrentMappedTrackInfo());
TrackGroupArray metadataTrackGroups =
mappedTrackInfo.getTrackGroups(METADATA_RENDERER_INDEX);
DefaultTrackSelector.SelectionOverride selectionOverride =
new DefaultTrackSelector.SelectionOverride(metadataTrack.mPlayerTrackIndex,
/* tracks...= */ 0);
mDefaultTrackSelector.setParameters(mDefaultTrackSelector.buildUponParameters()
.setRendererDisabled(METADATA_RENDERER_INDEX, /* disabled= */ false)
.setSelectionOverride(
METADATA_RENDERER_INDEX, metadataTrackGroups, selectionOverride)
.build());
return;
}
InternalTextTrackInfo textTrack = mTextTracks.get(trackId);
Preconditions.checkArgument(textTrack != null);
if (mPlayerTextTrackIndex != textTrack.mPlayerTrackIndex) {
// We need to do a player-level track selection.
mTextRenderer.clearSelection();
mPlayerTextTrackIndex = textTrack.mPlayerTrackIndex;
MappingTrackSelector.MappedTrackInfo mappedTrackInfo =
Preconditions.checkNotNull(mDefaultTrackSelector.getCurrentMappedTrackInfo());
TrackGroupArray textTrackGroups = mappedTrackInfo.getTrackGroups(TEXT_RENDERER_INDEX);
DefaultTrackSelector.SelectionOverride selectionOverride =
new DefaultTrackSelector.SelectionOverride(mPlayerTextTrackIndex, 0);
mDefaultTrackSelector.setParameters(mDefaultTrackSelector.buildUponParameters()
.setSelectionOverride(TEXT_RENDERER_INDEX, textTrackGroups, selectionOverride)
.build());
}
if (textTrack.mChannel != UNSET) {
mTextRenderer.select(textTrack.mType, textTrack.mChannel);
}
mSelectedTextTrack = textTrack;
}
public void deselectTrack(int trackId) {
InternalTrackInfo videoTrack = mVideoTracks.get(trackId);
Preconditions.checkArgument(videoTrack == null, "Video track deselection is not supported");
InternalTrackInfo audioTrack = mAudioTracks.get(trackId);
Preconditions.checkArgument(audioTrack == null, "Audio track deselection is not supported");
InternalTrackInfo metadataTrack = mMetadataTracks.get(trackId);
if (metadataTrack != null) {
mSelectedMetadataTrack = null;
mDefaultTrackSelector.setParameters(mDefaultTrackSelector.buildUponParameters()
.setRendererDisabled(METADATA_RENDERER_INDEX, /* disabled= */ true));
return;
}
Preconditions.checkArgument(mSelectedTextTrack != null
&& mSelectedTextTrack.mExternalTrackInfo.getId() == trackId);
mTextRenderer.clearSelection();
mSelectedTextTrack = null;
}
private static int getTextTrackType(String sampleMimeType) {
switch (sampleMimeType) {
case MimeTypes.APPLICATION_CEA608:
return TRACK_TYPE_CEA608;
case MimeTypes.APPLICATION_CEA708:
return TRACK_TYPE_CEA708;
case MimeTypes.TEXT_VTT:
return TRACK_TYPE_WEBVTT;
default:
throw new IllegalArgumentException("Unexpected text MIME type " + sampleMimeType);
}
}
static class InternalTrackInfo {
final int mPlayerTrackIndex;
final TrackInfo mExternalTrackInfo;
InternalTrackInfo(int playerTrackIndex, int trackInfoType, @Nullable MediaFormat format,
int trackId) {
mPlayerTrackIndex = playerTrackIndex;
mExternalTrackInfo = new TrackInfo(trackId, trackInfoType, format,
trackInfoType != MEDIA_TRACK_TYPE_VIDEO);
}
}
static final class InternalTextTrackInfo extends InternalTrackInfo {
static final String MIMETYPE_TEXT_CEA_608 = "text/cea-608";
static final String MIMETYPE_TEXT_CEA_708 = "text/cea-708";
static final int UNSET = -1;
final int mType;
final int mChannel;
@Nullable final Format mFormat;
InternalTextTrackInfo(int playerTrackIndex, @TextRenderer.TextTrackType int type,
@Nullable Format format, int channel, int trackId) {
super(playerTrackIndex, getTrackInfoType(type), getMediaFormat(type, format, channel),
trackId);
mType = type;
mChannel = channel;
mFormat = format;
}
private static int getTrackInfoType(@TextRenderer.TextTrackType int type) {
// Hide WebVTT tracks, like the NuPlayer-based implementation
// (see [internal: b/120081663]).
return type == TRACK_TYPE_WEBVTT ? TrackInfo.MEDIA_TRACK_TYPE_UNKNOWN
: TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE;
}
private static MediaFormat getMediaFormat(@TextRenderer.TextTrackType int type,
@Nullable Format format, int channel) {
@C.SelectionFlags int selectionFlags;
if (type == TRACK_TYPE_CEA608 && channel == 0) {
selectionFlags = C.SELECTION_FLAG_AUTOSELECT | C.SELECTION_FLAG_DEFAULT;
} else if (type == TRACK_TYPE_CEA708 && channel == 1) {
selectionFlags = C.SELECTION_FLAG_DEFAULT;
} else {
selectionFlags = format == null ? 0 : format.selectionFlags;
}
String language = format == null ? C.LANGUAGE_UNDETERMINED : format.language;
MediaFormat mediaFormat = new MediaFormat();
if (type == TRACK_TYPE_CEA608) {
mediaFormat.setString(MediaFormat.KEY_MIME, MIMETYPE_TEXT_CEA_608);
} else if (type == TRACK_TYPE_CEA708) {
mediaFormat.setString(MediaFormat.KEY_MIME, MIMETYPE_TEXT_CEA_708);
} else if (type == TRACK_TYPE_WEBVTT) {
mediaFormat.setString(MediaFormat.KEY_MIME, MimeTypes.TEXT_VTT);
} else {
// Unexpected.
throw new IllegalStateException();
}
mediaFormat.setString(MediaFormat.KEY_LANGUAGE, language);
mediaFormat.setInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE,
(selectionFlags & C.SELECTION_FLAG_FORCED) != 0 ? 1 : 0);
mediaFormat.setInteger(MediaFormat.KEY_IS_AUTOSELECT,
(selectionFlags & C.SELECTION_FLAG_AUTOSELECT) != 0 ? 1 : 0);
mediaFormat.setInteger(MediaFormat.KEY_IS_DEFAULT,
(selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0 ? 1 : 0);
return mediaFormat;
}
}
}