Merge pull request #6178 from xirac:feature/text-track-score

PiperOrigin-RevId: 259707359
This commit is contained in:
Toni 2019-07-24 12:33:37 +01:00
commit feb807897f
2 changed files with 146 additions and 85 deletions

View File

@ -19,7 +19,6 @@ import android.content.Context;
import android.graphics.Point; import android.graphics.Point;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Pair; import android.util.Pair;
@ -1552,7 +1551,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} }
} }
int selectedTextTrackScore = Integer.MIN_VALUE; TextTrackScore selectedTextTrackScore = null;
int selectedTextRendererIndex = C.INDEX_UNSET; int selectedTextRendererIndex = C.INDEX_UNSET;
for (int i = 0; i < rendererCount; i++) { for (int i = 0; i < rendererCount; i++) {
int trackType = mappedTrackInfo.getRendererType(i); int trackType = mappedTrackInfo.getRendererType(i);
@ -1562,13 +1561,15 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// Already done. Do nothing. // Already done. Do nothing.
break; break;
case C.TRACK_TYPE_TEXT: case C.TRACK_TYPE_TEXT:
Pair<TrackSelection.Definition, Integer> textSelection = Pair<TrackSelection.Definition, TextTrackScore> textSelection =
selectTextTrack( selectTextTrack(
mappedTrackInfo.getTrackGroups(i), mappedTrackInfo.getTrackGroups(i),
rendererFormatSupports[i], rendererFormatSupports[i],
params, params,
selectedAudioLanguage); selectedAudioLanguage);
if (textSelection != null && textSelection.second > selectedTextTrackScore) { if (textSelection != null
&& (selectedTextTrackScore == null
|| textSelection.second.compareTo(selectedTextTrackScore) > 0)) {
if (selectedTextRendererIndex != C.INDEX_UNSET) { if (selectedTextRendererIndex != C.INDEX_UNSET) {
// We've already made a selection for another text renderer, but it had a lower score. // We've already made a selection for another text renderer, but it had a lower score.
// Clear the selection for that renderer. // Clear the selection for that renderer.
@ -2052,21 +2053,21 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* track, indexed by track group index and track index (in that order). * track, indexed by track group index and track index (in that order).
* @param params The selector's current constraint parameters. * @param params The selector's current constraint parameters.
* @param selectedAudioLanguage The language of the selected audio track. May be null if the * @param selectedAudioLanguage The language of the selected audio track. May be null if the
* selected audio track declares no language or no audio track was selected. * selected text track declares no language or no text track was selected.
* @return The {@link TrackSelection.Definition} and corresponding track score, or null if no * @return The {@link TrackSelection.Definition} and corresponding {@link TextTrackScore}, or null
* selection was made. * if no selection was made.
* @throws ExoPlaybackException If an error occurs while selecting the tracks. * @throws ExoPlaybackException If an error occurs while selecting the tracks.
*/ */
@Nullable @Nullable
protected Pair<TrackSelection.Definition, Integer> selectTextTrack( protected Pair<TrackSelection.Definition, TextTrackScore> selectTextTrack(
TrackGroupArray groups, TrackGroupArray groups,
int[][] formatSupport, int[][] formatSupport,
Parameters params, Parameters params,
@Nullable String selectedAudioLanguage) @Nullable String selectedAudioLanguage)
throws ExoPlaybackException { throws ExoPlaybackException {
TrackGroup selectedGroup = null; TrackGroup selectedGroup = null;
int selectedTrackIndex = 0; int selectedTrackIndex = C.INDEX_UNSET;
int selectedTrackScore = 0; TextTrackScore selectedTrackScore = null;
for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
TrackGroup trackGroup = groups.get(groupIndex); TrackGroup trackGroup = groups.get(groupIndex);
int[] trackFormatSupport = formatSupport[groupIndex]; int[] trackFormatSupport = formatSupport[groupIndex];
@ -2074,39 +2075,11 @@ public class DefaultTrackSelector extends MappingTrackSelector {
if (isSupported(trackFormatSupport[trackIndex], if (isSupported(trackFormatSupport[trackIndex],
params.exceedRendererCapabilitiesIfNecessary)) { params.exceedRendererCapabilitiesIfNecessary)) {
Format format = trackGroup.getFormat(trackIndex); Format format = trackGroup.getFormat(trackIndex);
int maskedSelectionFlags = TextTrackScore trackScore =
format.selectionFlags & ~params.disabledTextTrackSelectionFlags; new TextTrackScore(
boolean isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; format, params, trackFormatSupport[trackIndex], selectedAudioLanguage);
boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; if (trackScore.isWithinConstraints
int trackScore; && (selectedTrackScore == null || trackScore.compareTo(selectedTrackScore) > 0)) {
int languageScore = getFormatLanguageScore(format, params.preferredTextLanguage);
boolean trackHasNoLanguage = formatHasNoLanguage(format);
if (languageScore > 0 || (params.selectUndeterminedTextLanguage && trackHasNoLanguage)) {
if (isDefault) {
trackScore = 11;
} else if (!isForced) {
// Prefer non-forced to forced if a preferred text language has been specified. Where
// both are provided the non-forced track will usually contain the forced subtitles as
// a subset.
trackScore = 7;
} else {
trackScore = 3;
}
trackScore += languageScore;
} else if (isDefault) {
trackScore = 2;
} else if (isForced
&& (getFormatLanguageScore(format, selectedAudioLanguage) > 0
|| (trackHasNoLanguage && stringDefinesNoLanguage(selectedAudioLanguage)))) {
trackScore = 1;
} else {
// Track should not be selected.
continue;
}
if (isSupported(trackFormatSupport[trackIndex], false)) {
trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS;
}
if (trackScore > selectedTrackScore) {
selectedGroup = trackGroup; selectedGroup = trackGroup;
selectedTrackIndex = trackIndex; selectedTrackIndex = trackIndex;
selectedTrackScore = trackScore; selectedTrackScore = trackScore;
@ -2117,7 +2090,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return selectedGroup == null return selectedGroup == null
? null ? null
: Pair.create( : Pair.create(
new TrackSelection.Definition(selectedGroup, selectedTrackIndex), selectedTrackScore); new TrackSelection.Definition(selectedGroup, selectedTrackIndex),
Assertions.checkNotNull(selectedTrackScore));
} }
// General track selection methods. // General track selection methods.
@ -2287,19 +2261,17 @@ public class DefaultTrackSelector extends MappingTrackSelector {
&& maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES);
} }
/** Equivalent to {@link #stringDefinesNoLanguage stringDefinesNoLanguage(format.language)}. */
protected static boolean formatHasNoLanguage(Format format) {
return stringDefinesNoLanguage(format.language);
}
/** /**
* Returns whether the given string does not define a language. * Normalizes the input string to null if it does not define a language, or returns it otherwise.
* *
* @param language The string. * @param language The string.
* @return Whether the given string does not define a language. * @return The string, optionally normalized to null if it does not define a language.
*/ */
protected static boolean stringDefinesNoLanguage(@Nullable String language) { @Nullable
return TextUtils.isEmpty(language) || TextUtils.equals(language, C.LANGUAGE_UNDETERMINED); protected static String normalizeUndeterminedLanguageToNull(@Nullable String language) {
return TextUtils.isEmpty(language) || TextUtils.equals(language, C.LANGUAGE_UNDETERMINED)
? null
: language;
} }
/** /**
@ -2307,26 +2279,34 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* *
* @param format The {@link Format}. * @param format The {@link Format}.
* @param language The language, or null. * @param language The language, or null.
* @return A score of 3 if the languages match fully, a score of 2 if the languages match partly, * @param allowUndeterminedFormatLanguage Whether matches with an empty or undetermined format
* a score of 1 if the languages don't match but belong to the same main language, and a score * language tag are allowed.
* of 0 if the languages don't match at all. * @return A score of 4 if the languages match fully, a score of 3 if the languages match partly,
* a score of 2 if the languages don't match but belong to the same main language, a score of
* 1 if the format language is undetermined and such a match is allowed, and a score of 0 if
* the languages don't match at all.
*/ */
protected static int getFormatLanguageScore(Format format, @Nullable String language) { protected static int getFormatLanguageScore(
if (format.language == null || language == null) { Format format, @Nullable String language, boolean allowUndeterminedFormatLanguage) {
return 0; if (!TextUtils.isEmpty(language) && language.equals(format.language)) {
// Full literal match of non-empty languages, including matches of an explicit "und" query.
return 4;
} }
if (TextUtils.equals(format.language, language)) { language = normalizeUndeterminedLanguageToNull(language);
String formatLanguage = normalizeUndeterminedLanguageToNull(format.language);
if (formatLanguage == null || language == null) {
// At least one of the languages is undetermined.
return allowUndeterminedFormatLanguage && formatLanguage == null ? 1 : 0;
}
if (formatLanguage.startsWith(language) || language.startsWith(formatLanguage)) {
// Partial match where one language is a subset of the other (e.g. "zh-hans" and "zh-hans-hk")
return 3; return 3;
} }
// Partial match where one language is a subset of the other (e.g. "zh-hans" and "zh-hans-hk") String formatMainLanguage = Util.splitAtFirst(formatLanguage, "-")[0];
if (format.language.startsWith(language) || language.startsWith(format.language)) {
return 2;
}
// Partial match where only the main language tag is the same (e.g. "fr-fr" and "fr-ca")
String formatMainLanguage = Util.splitAtFirst(format.language, "-")[0];
String queryMainLanguage = Util.splitAtFirst(language, "-")[0]; String queryMainLanguage = Util.splitAtFirst(language, "-")[0];
if (formatMainLanguage.equals(queryMainLanguage)) { if (formatMainLanguage.equals(queryMainLanguage)) {
return 1; // Partial match where only the main language tag is the same (e.g. "fr-fr" and "fr-ca")
return 2;
} }
return 0; return 0;
} }
@ -2400,9 +2380,25 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} }
} }
/**
* Compares two integers in a safe way avoiding potential overflow.
*
* @param first The first value.
* @param second The second value.
* @return A negative integer if the first value is less than the second. Zero if they are equal.
* A positive integer if the first value is greater than the second.
*/
private static int compareInts(int first, int second) {
return first > second ? 1 : (second > first ? -1 : 0);
}
/** Represents how well an audio track matches the selection {@link Parameters}. */ /** Represents how well an audio track matches the selection {@link Parameters}. */
protected static final class AudioTrackScore implements Comparable<AudioTrackScore> { protected static final class AudioTrackScore implements Comparable<AudioTrackScore> {
/**
* Whether the provided format is within the parameter constraints. If {@code false}, the format
* should not be selected.
*/
public final boolean isWithinConstraints; public final boolean isWithinConstraints;
private final Parameters parameters; private final Parameters parameters;
@ -2418,7 +2414,11 @@ public class DefaultTrackSelector extends MappingTrackSelector {
public AudioTrackScore(Format format, Parameters parameters, int formatSupport) { public AudioTrackScore(Format format, Parameters parameters, int formatSupport) {
this.parameters = parameters; this.parameters = parameters;
isWithinRendererCapabilities = isSupported(formatSupport, false); isWithinRendererCapabilities = isSupported(formatSupport, false);
preferredLanguageScore = getFormatLanguageScore(format, parameters.preferredAudioLanguage); preferredLanguageScore =
getFormatLanguageScore(
format,
parameters.preferredAudioLanguage,
/* allowUndeterminedFormatLanguage= */ false);
isDefaultSelectionFlag = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; isDefaultSelectionFlag = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
channelCount = format.channelCount; channelCount = format.channelCount;
sampleRate = format.sampleRate; sampleRate = format.sampleRate;
@ -2431,7 +2431,9 @@ public class DefaultTrackSelector extends MappingTrackSelector {
int bestMatchIndex = Integer.MAX_VALUE; int bestMatchIndex = Integer.MAX_VALUE;
int bestMatchScore = 0; int bestMatchScore = 0;
for (int i = 0; i < localeLanguages.length; i++) { for (int i = 0; i < localeLanguages.length; i++) {
int score = getFormatLanguageScore(format, localeLanguages[i]); int score =
getFormatLanguageScore(
format, localeLanguages[i], /* allowUndeterminedFormatLanguage= */ false);
if (score > 0) { if (score > 0) {
bestMatchIndex = i; bestMatchIndex = i;
bestMatchScore = score; bestMatchScore = score;
@ -2450,7 +2452,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* negative integer if this score is worse than the other. * negative integer if this score is worse than the other.
*/ */
@Override @Override
public int compareTo(@NonNull AudioTrackScore other) { public int compareTo(AudioTrackScore other) {
if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) { if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) {
return this.isWithinRendererCapabilities ? 1 : -1; return this.isWithinRendererCapabilities ? 1 : -1;
} }
@ -2488,18 +2490,6 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} }
} }
/**
* Compares two integers in a safe way and avoiding potential overflow.
*
* @param first The first value.
* @param second The second value.
* @return A negative integer if the first value is less than the second. Zero if they are equal.
* A positive integer if the first value is greater than the second.
*/
private static int compareInts(int first, int second) {
return first > second ? 1 : (second > first ? -1 : 0);
}
private static final class AudioConfigurationTuple { private static final class AudioConfigurationTuple {
public final int channelCount; public final int channelCount;
@ -2535,4 +2525,75 @@ public class DefaultTrackSelector extends MappingTrackSelector {
} }
/** Represents how well a text track matches the selection {@link Parameters}. */
protected static final class TextTrackScore implements Comparable<TextTrackScore> {
/**
* Whether the provided format is within the parameter constraints. If {@code false}, the format
* should not be selected.
*/
public final boolean isWithinConstraints;
private final boolean isWithinRendererCapabilities;
private final boolean isDefault;
private final boolean isForced;
private final int preferredLanguageScore;
private final boolean isForcedAndSelectedAudioLanguage;
public TextTrackScore(
Format format,
Parameters parameters,
int trackFormatSupport,
@Nullable String selectedAudioLanguage) {
isWithinRendererCapabilities =
isSupported(trackFormatSupport, /* allowExceedsCapabilities= */ false);
int maskedSelectionFlags =
format.selectionFlags & ~parameters.disabledTextTrackSelectionFlags;
isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0;
preferredLanguageScore =
getFormatLanguageScore(
format, parameters.preferredTextLanguage, parameters.selectUndeterminedTextLanguage);
boolean selectedAudioLanguageUndetermined =
normalizeUndeterminedLanguageToNull(selectedAudioLanguage) == null;
int selectedAudioLanguageScore =
getFormatLanguageScore(format, selectedAudioLanguage, selectedAudioLanguageUndetermined);
isForcedAndSelectedAudioLanguage = isForced && selectedAudioLanguageScore > 0;
isWithinConstraints =
preferredLanguageScore > 0 || isDefault || isForcedAndSelectedAudioLanguage;
}
/**
* Compares this score with another.
*
* @param other The other score to compare to.
* @return A positive integer if this score is better than the other. Zero if they are equal. A
* negative integer if this score is worse than the other.
*/
@Override
public int compareTo(TextTrackScore other) {
if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) {
return this.isWithinRendererCapabilities ? 1 : -1;
}
if ((this.preferredLanguageScore > 0) != (other.preferredLanguageScore > 0)) {
return this.preferredLanguageScore > 0 ? 1 : -1;
}
if (this.isDefault != other.isDefault) {
return this.isDefault ? 1 : -1;
}
if (this.preferredLanguageScore > 0) {
if (this.isForced != other.isForced) {
// Prefer non-forced to forced if a preferred text language has been specified. Where
// both are provided the non-forced track will usually contain the forced subtitles as
// a subset.
return !this.isForced ? 1 : -1;
}
return compareInts(this.preferredLanguageScore, other.preferredLanguageScore);
}
if (this.isForcedAndSelectedAudioLanguage != other.isForcedAndSelectedAudioLanguage) {
return this.isForcedAndSelectedAudioLanguage ? 1 : -1;
}
return 0;
}
}
} }

View File

@ -1002,12 +1002,12 @@ public final class DefaultTrackSelectorTest {
result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE);
assertNoSelection(result.selections.get(0)); assertNoSelection(result.selections.get(0));
// There is a preferred language, so the first language-matching track flagged as default should // There is a preferred language, so a language-matching track flagged as default should
// be selected. // be selected, and the one without forced flag should be preferred.
trackSelector.setParameters( trackSelector.setParameters(
Parameters.DEFAULT.buildUpon().setPreferredTextLanguage("eng").build()); Parameters.DEFAULT.buildUpon().setPreferredTextLanguage("eng").build());
result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE); result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE);
assertFixedSelection(result.selections.get(0), trackGroups, forcedDefault); assertFixedSelection(result.selections.get(0), trackGroups, defaultOnly);
// Same as above, but the default flag is disabled. If multiple tracks match the preferred // Same as above, but the default flag is disabled. If multiple tracks match the preferred
// language, those not flagged as forced are preferred, as they likely include the contents of // language, those not flagged as forced are preferred, as they likely include the contents of