/*
* Copyright 2018 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.exoplayer;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static androidx.media2.MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_AUDIO;
import static androidx.media2.MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_METADATA;
import static androidx.media2.MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE;
import static androidx.media2.MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT;
import static androidx.media2.MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_UNKNOWN;
import static androidx.media2.MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_VIDEO;
import static androidx.media2.exoplayer.RenderersFactory.AUDIO_RENDERER_INDEX;
import static androidx.media2.exoplayer.RenderersFactory.METADATA_RENDERER_INDEX;
import static androidx.media2.exoplayer.RenderersFactory.TEXT_RENDERER_INDEX;
import static androidx.media2.exoplayer.RenderersFactory.VIDEO_RENDERER_INDEX;
import static androidx.media2.exoplayer.TextRenderer.TRACK_TYPE_CEA608;
import static androidx.media2.exoplayer.TextRenderer.TRACK_TYPE_CEA708;
import static androidx.media2.exoplayer.TextRenderer.TRACK_TYPE_WEBVTT;
import static androidx.media2.exoplayer.TrackSelector.InternalTextTrackInfo.UNSET;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.media.MediaFormat;
import android.os.Build;
import androidx.annotation.RestrictTo;
import androidx.core.util.Preconditions;
import androidx.media2.MediaPlayer2;
import androidx.media2.common.TrackInfoImpl;
import androidx.media2.exoplayer.external.C;
import androidx.media2.exoplayer.external.Format;
import androidx.media2.exoplayer.external.Player;
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.List;
/**
* Manages track selection for {@link ExoPlayerWrapper}.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@TargetApi(Build.VERSION_CODES.KITKAT)
@SuppressLint("RestrictedApi") // TODO(b/68398926): Remove once RestrictedApi checks are fixed.
/* package */ final class TrackSelector {
private static final int TRACK_INDEX_UNSET = -1;
private final TextRenderer mTextRenderer;
private final DefaultTrackSelector mDefaultTrackSelector;
private final List<MediaPlayer2.TrackInfo> mAudioTrackInfos;
private final List<MediaPlayer2.TrackInfo> mVideoTrackInfos;
private final List<MediaPlayer2.TrackInfo> mMetadataTrackInfos;
private final List<MediaPlayer2.TrackInfo> mTextTrackInfos;
private final List<InternalTextTrackInfo> mInternalTextTrackInfos;
private boolean mPendingMetadataUpdate;
private int mSelectedAudioTrackIndex;
private int mSelectedVideoTrackIndex;
private int mSelectedMetadataTrackIndex;
private int mPlayerTextTrackIndex;
private int mSelectedTextTrackIndex;
TrackSelector(TextRenderer textRenderer) {
mTextRenderer = textRenderer;
mDefaultTrackSelector = new DefaultTrackSelector();
mAudioTrackInfos = new ArrayList<>();
mVideoTrackInfos = new ArrayList<>();
mMetadataTrackInfos = new ArrayList<>();
mTextTrackInfos = new ArrayList<>();
mInternalTextTrackInfos = new ArrayList<>();
mSelectedAudioTrackIndex = TRACK_INDEX_UNSET;
mSelectedVideoTrackIndex = TRACK_INDEX_UNSET;
mSelectedMetadataTrackIndex = TRACK_INDEX_UNSET;
mPlayerTextTrackIndex = TRACK_INDEX_UNSET;
mSelectedTextTrackIndex = 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(Player player) {
mPendingMetadataUpdate = true;
// Clear all selection state.
mDefaultTrackSelector.setParameters(
mDefaultTrackSelector.buildUponParameters().clearSelectionOverrides());
mSelectedAudioTrackIndex = TRACK_INDEX_UNSET;
mSelectedVideoTrackIndex = TRACK_INDEX_UNSET;
mSelectedMetadataTrackIndex = TRACK_INDEX_UNSET;
mPlayerTextTrackIndex = TRACK_INDEX_UNSET;
mSelectedTextTrackIndex = TRACK_INDEX_UNSET;
mAudioTrackInfos.clear();
mVideoTrackInfos.clear();
mMetadataTrackInfos.clear();
mInternalTextTrackInfos.clear();
mTextRenderer.clearSelection();
MappingTrackSelector.MappedTrackInfo mappedTrackInfo =
mDefaultTrackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo == null) {
return;
}
// Enumerate track information.
TrackGroupArray audioTrackGroups = mappedTrackInfo.getTrackGroups(AUDIO_RENDERER_INDEX);
for (int i = 0; i < audioTrackGroups.length; i++) {
TrackGroup trackGroup = audioTrackGroups.get(i);
TrackInfoImpl trackInfo = new TrackInfoImpl(
MEDIA_TRACK_TYPE_AUDIO, ExoPlayerUtils.getMediaFormat(trackGroup.getFormat(0)));
mAudioTrackInfos.add(trackInfo);
}
TrackGroupArray videoTrackGroups = mappedTrackInfo.getTrackGroups(VIDEO_RENDERER_INDEX);
for (int i = 0; i < videoTrackGroups.length; i++) {
TrackGroup trackGroup = videoTrackGroups.get(i);
TrackInfoImpl trackInfo = new TrackInfoImpl(
MEDIA_TRACK_TYPE_VIDEO, ExoPlayerUtils.getMediaFormat(trackGroup.getFormat(0)));
mVideoTrackInfos.add(trackInfo);
}
TrackGroupArray metadataTrackGroups =
mappedTrackInfo.getTrackGroups(METADATA_RENDERER_INDEX);
for (int i = 0; i < metadataTrackGroups.length; i++) {
TrackGroup trackGroup = metadataTrackGroups.get(i);
TrackInfoImpl trackInfo = new TrackInfoImpl(
MEDIA_TRACK_TYPE_METADATA,
ExoPlayerUtils.getMediaFormat(trackGroup.getFormat(0)));
mMetadataTrackInfos.add(trackInfo);
}
// Determine selected track indices for audio and video.
TrackSelectionArray trackSelections = player.getCurrentTrackSelections();
TrackSelection audioTrackSelection = trackSelections.get(AUDIO_RENDERER_INDEX);
mSelectedAudioTrackIndex = audioTrackSelection == null
? TRACK_INDEX_UNSET : audioTrackGroups.indexOf(audioTrackSelection.getTrackGroup());
TrackSelection videoTrackSelection = trackSelections.get(VIDEO_RENDERER_INDEX);
mSelectedVideoTrackIndex = videoTrackSelection == null
? TRACK_INDEX_UNSET : videoTrackGroups.indexOf(videoTrackSelection.getTrackGroup());
TrackSelection metadataTrackSelection = trackSelections.get(METADATA_RENDERER_INDEX);
mSelectedMetadataTrackIndex = metadataTrackSelection == null
? TRACK_INDEX_UNSET : metadataTrackGroups.indexOf(
metadataTrackSelection.getTrackGroup());
// 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 = 0; i < textTrackGroups.length; i++) {
TrackGroup trackGroup = textTrackGroups.get(i);
InternalTextTrackInfo internalTextTrackInfo =
new InternalTextTrackInfo(i, trackGroup.getFormat(0));
mInternalTextTrackInfos.add(internalTextTrackInfo);
mTextTrackInfos.add(internalTextTrackInfo.mTrackInfo);
}
TrackSelection textTrackSelection = trackSelections.get(TEXT_RENDERER_INDEX);
mPlayerTextTrackIndex = textTrackSelection == null
? TRACK_INDEX_UNSET : textTrackGroups.indexOf(textTrackSelection.getTrackGroup());
}
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 < mInternalTextTrackInfos.size(); i++) {
InternalTextTrackInfo internalTextTrackInfo = mInternalTextTrackInfos.get(i);
if (internalTextTrackInfo.mType == type && internalTextTrackInfo.mChannel == UNSET) {
// Associate the existing text track with this channel.
InternalTextTrackInfo replacementTextTrackInfo = new InternalTextTrackInfo(
internalTextTrackInfo.mPlayerTrackIndex, type, channel);
mInternalTextTrackInfos.set(i, replacementTextTrackInfo);
if (mSelectedTextTrackIndex == i) {
mTextRenderer.select(type, channel);
}
populatedExistingTrack = true;
break;
}
}
if (!populatedExistingTrack) {
InternalTextTrackInfo internalTextTrackInfo =
new InternalTextTrackInfo(mPlayerTextTrackIndex, type, channel);
mInternalTextTrackInfos.add(internalTextTrackInfo);
mTextTrackInfos.add(internalTextTrackInfo.mTrackInfo);
mPendingMetadataUpdate = true;
}
}
public boolean hasPendingMetadataUpdate() {
boolean pendingMetadataUpdate = mPendingMetadataUpdate;
mPendingMetadataUpdate = false;
return pendingMetadataUpdate;
}
public int getSelectedTrack(int trackType) {
switch (trackType) {
case MEDIA_TRACK_TYPE_AUDIO:
return mSelectedAudioTrackIndex;
case MEDIA_TRACK_TYPE_VIDEO:
return mAudioTrackInfos.size() + mSelectedVideoTrackIndex;
case MEDIA_TRACK_TYPE_METADATA:
return mAudioTrackInfos.size() + mVideoTrackInfos.size()
+ mSelectedMetadataTrackIndex;
case MEDIA_TRACK_TYPE_SUBTITLE:
return mAudioTrackInfos.size() + mVideoTrackInfos.size()
+ mMetadataTrackInfos.size() + mSelectedTextTrackIndex;
case MEDIA_TRACK_TYPE_TIMEDTEXT:
case MEDIA_TRACK_TYPE_UNKNOWN:
default:
return TRACK_INDEX_UNSET;
}
}
public List<MediaPlayer2.TrackInfo> getTrackInfos() {
ArrayList<MediaPlayer2.TrackInfo> trackInfos = new ArrayList<>(
mVideoTrackInfos.size() + mAudioTrackInfos.size() + mMetadataTrackInfos.size()
+ mInternalTextTrackInfos.size());
trackInfos.addAll(mVideoTrackInfos);
trackInfos.addAll(mAudioTrackInfos);
trackInfos.addAll(mMetadataTrackInfos);
trackInfos.addAll(mTextTrackInfos);
// Note: the list returned by MediaPlayer2Impl is modifiable so do the same here.
return trackInfos;
}
public void selectTrack(int index) {
Preconditions.checkArgument(
index >= mVideoTrackInfos.size(), "Video track selection is not supported");
index -= mVideoTrackInfos.size();
if (index < mAudioTrackInfos.size()) {
mSelectedAudioTrackIndex = index;
MappingTrackSelector.MappedTrackInfo mappedTrackInfo =
Preconditions.checkNotNull(mDefaultTrackSelector.getCurrentMappedTrackInfo());
TrackGroupArray audioTrackGroups = mappedTrackInfo.getTrackGroups(AUDIO_RENDERER_INDEX);
TrackGroup selectedTrackGroup = audioTrackGroups.get(index);
// 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(index, trackIndices);
mDefaultTrackSelector.setParameters(mDefaultTrackSelector.buildUponParameters()
.setSelectionOverride(AUDIO_RENDERER_INDEX, audioTrackGroups, selectionOverride)
.build());
return;
}
index -= mAudioTrackInfos.size();
if (index < mMetadataTrackInfos.size()) {
mSelectedMetadataTrackIndex = index;
MappingTrackSelector.MappedTrackInfo mappedTrackInfo =
Preconditions.checkNotNull(mDefaultTrackSelector.getCurrentMappedTrackInfo());
TrackGroupArray metadataTrackGroups =
mappedTrackInfo.getTrackGroups(METADATA_RENDERER_INDEX);
DefaultTrackSelector.SelectionOverride selectionOverride =
new DefaultTrackSelector.SelectionOverride(index, /* tracks= */ 0);
mDefaultTrackSelector.setParameters(mDefaultTrackSelector.buildUponParameters()
.setRendererDisabled(METADATA_RENDERER_INDEX, /* disabled= */ false)
.setSelectionOverride(
METADATA_RENDERER_INDEX, metadataTrackGroups, selectionOverride)
.build());
return;
}
index -= mMetadataTrackInfos.size();
Preconditions.checkArgument(index < mInternalTextTrackInfos.size());
InternalTextTrackInfo internalTextTrackInfo = mInternalTextTrackInfos.get(index);
if (mPlayerTextTrackIndex != internalTextTrackInfo.mPlayerTrackIndex) {
// We need to do a player-level track selection.
mTextRenderer.clearSelection();
mPlayerTextTrackIndex = internalTextTrackInfo.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 (internalTextTrackInfo.mChannel != UNSET) {
mTextRenderer.select(internalTextTrackInfo.mType, internalTextTrackInfo.mChannel);
}
mSelectedTextTrackIndex = index;
}
public void deselectTrack(int index) {
Preconditions.checkArgument(
index >= mVideoTrackInfos.size(), "Video track deselection is not supported");
index -= mVideoTrackInfos.size();
Preconditions.checkArgument(
index >= mAudioTrackInfos.size(), "Audio track deselection is not supported");
index -= mAudioTrackInfos.size();
if (index < mMetadataTrackInfos.size()) {
mSelectedMetadataTrackIndex = TRACK_INDEX_UNSET;
mDefaultTrackSelector.setParameters(mDefaultTrackSelector.buildUponParameters()
.setRendererDisabled(METADATA_RENDERER_INDEX, /* disabled= */ true));
return;
}
index -= mMetadataTrackInfos.size();
Preconditions.checkArgument(index == mSelectedTextTrackIndex);
mTextRenderer.clearSelection();
mSelectedTextTrackIndex = TRACK_INDEX_UNSET;
}
public static final class InternalTextTrackInfo {
public static final String MIMETYPE_TEXT_CEA_608 = "text/cea-608";
public static final String MIMETYPE_TEXT_CEA_708 = "text/cea-708";
public static final int UNSET = -1;
public final int mPlayerTrackIndex;
public final TrackInfoImpl mTrackInfo;
public final int mType;
public final int mChannel;
InternalTextTrackInfo(int playerTrackIndex, Format format) {
mPlayerTrackIndex = playerTrackIndex;
if (MimeTypes.APPLICATION_CEA608.equals(format.sampleMimeType)) {
mType = TRACK_TYPE_CEA608;
} else if (MimeTypes.APPLICATION_CEA708.equals(format.sampleMimeType)) {
mType = TRACK_TYPE_CEA708;
} else if (MimeTypes.TEXT_VTT.equals(format.sampleMimeType)) {
mType = TRACK_TYPE_WEBVTT;
} else {
throw new IllegalStateException();
}
mTrackInfo = getTrackInfo(
mType,
(format.selectionFlags & C.SELECTION_FLAG_AUTOSELECT) != 0,
(format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0);
mChannel = UNSET;
}
// TODO(b/80232248): Set flags for WebVTT.
InternalTextTrackInfo(int playerTrackIndex, int type, int channel) {
mPlayerTrackIndex = playerTrackIndex;
mType = type;
mChannel = channel;
mTrackInfo = getTrackInfo(
mType,
/* isDefaultAuto= */ mType == TRACK_TYPE_CEA608 && mChannel == 0,
/* isDefaultOnly= */ mType == TRACK_TYPE_CEA708 && mChannel == 1);
}
static TrackInfoImpl getTrackInfo(int type, boolean isDefaultAuto, boolean isDefaultOnly) {
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, C.LANGUAGE_UNDETERMINED);
mediaFormat.setInteger(MediaFormat.KEY_IS_AUTOSELECT, isDefaultAuto ? 1 : 0);
mediaFormat.setInteger(MediaFormat.KEY_IS_DEFAULT,
isDefaultAuto || isDefaultOnly ? 1 : 0);
return new TrackInfoImpl(MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE, mediaFormat);
}
}
}