Switch format language tag to use full BCP 47 codes.

This allows to distinguish between regional variants and scripts.

We still need to normalize the language code itself to make track selection
independent of the whether 2 or 3 letter codes are used.

PiperOrigin-RevId: 239783115
This commit is contained in:
tonihei 2019-03-22 13:13:34 +00:00 committed by Oliver Woodman
parent 3543116da4
commit e5aed73fba
9 changed files with 71 additions and 35 deletions

View File

@ -80,6 +80,7 @@
deprecated.
* Prevent seeking when ICY metadata is present to prevent playback problems
([#5658](https://github.com/google/ExoPlayer/issues/5658)).
* Use full BCP 47 language tags in `Format`.
### 2.9.6 ###

View File

@ -156,7 +156,7 @@ public final class Format implements Parcelable {
// Audio and text specific.
/** The language as ISO 639-2/T three-letter code, or null if unknown or not applicable. */
/** The language as an IETF BCP 47 conformant tag, or null if unknown or not applicable. */
public final @Nullable String language;
/**
* The Accessibility channel, or {@link #NO_VALUE} if not known or applicable.

View File

@ -495,7 +495,7 @@ public final class DownloadHelper {
* used instead. Must not be called until after preparation completes.
*
* @param languages A list of audio languages for which tracks should be added to the download
* selection, as ISO 639-1 two-letter or ISO 639-2 three-letter codes.
* selection, as IETF BCP 47 conformant tags.
*/
public void addAudioLanguagesToSelection(String... languages) {
assertPreparedWithMedia();
@ -524,7 +524,7 @@ public final class DownloadHelper {
* selected for downloading if no track with one of the specified {@code languages} is
* available.
* @param languages A list of text languages for which tracks should be added to the download
* selection, as ISO 639-1 two-letter or ISO 639-2 three-letter codes.
* selection, as IETF BCP 47 conformant tags.
*/
public void addTextLanguagesToSelection(
boolean selectUndeterminedTextLanguage, String... languages) {

View File

@ -2017,28 +2017,24 @@ public class DefaultTrackSelector extends MappingTrackSelector {
boolean isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0;
int trackScore;
boolean preferredLanguageFound = formatHasLanguage(format, params.preferredTextLanguage);
if (preferredLanguageFound
int languageScore = getFormatLanguageScore(format, params.preferredTextLanguage);
if (languageScore > 0
|| (params.selectUndeterminedTextLanguage && formatHasNoLanguage(format))) {
if (isDefault) {
trackScore = 8;
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 = 6;
trackScore = 8;
} else {
trackScore = 4;
trackScore = 5;
}
trackScore += preferredLanguageFound ? 1 : 0;
trackScore += languageScore;
} else if (isDefault) {
trackScore = 3;
trackScore = 4;
} else if (isForced) {
if (formatHasLanguage(format, params.preferredAudioLanguage)) {
trackScore = 2;
} else {
trackScore = 1;
}
trackScore = 1 + languageScore;
} else {
// Track should not be selected.
continue;
@ -2234,20 +2230,26 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* @return Whether the {@link Format} does not define a language.
*/
protected static boolean formatHasNoLanguage(Format format) {
return TextUtils.isEmpty(format.language) || formatHasLanguage(format, C.LANGUAGE_UNDETERMINED);
return TextUtils.isEmpty(format.language)
|| TextUtils.equals(format.language, C.LANGUAGE_UNDETERMINED);
}
/**
* Returns whether a {@link Format} specifies a particular language, or {@code false} if {@code
* language} is null.
* Returns a score for how well a language specified in a {@link Format} fits a given language.
*
* @param format The {@link Format}.
* @param language The language.
* @return Whether the format specifies the language, or {@code false} if {@code language} is
* null.
* @param language The language, or null.
* @return A score of 0 if the languages don't fit, a score of 1 if the languages fit partly and a
* score of 2 if the languages fit fully.
*/
protected static boolean formatHasLanguage(Format format, @Nullable String language) {
return language != null && TextUtils.equals(language, format.language);
protected static int getFormatLanguageScore(Format format, @Nullable String language) {
if (language == null) {
return 0;
}
if (TextUtils.equals(language, format.language)) {
return 2;
}
return format.language != null && format.language.startsWith(language) ? 1 : 0;
}
private static List<Integer> getViewportFilteredTrackIndices(TrackGroup group, int viewportWidth,
@ -2335,7 +2337,7 @@ public class DefaultTrackSelector extends MappingTrackSelector {
public AudioTrackScore(Format format, Parameters parameters, int formatSupport) {
this.parameters = parameters;
withinRendererCapabilitiesScore = isSupported(formatSupport, false) ? 1 : 0;
matchLanguageScore = formatHasLanguage(format, parameters.preferredAudioLanguage) ? 1 : 0;
matchLanguageScore = getFormatLanguageScore(format, parameters.preferredAudioLanguage);
defaultSelectionFlagScore = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0 ? 1 : 0;
channelCount = format.channelCount;
sampleRate = format.sampleRate;

View File

@ -59,8 +59,7 @@ public class TrackSelectionParameters implements Parcelable {
/**
* See {@link TrackSelectionParameters#preferredAudioLanguage}.
*
* @param preferredAudioLanguage Preferred audio language as an ISO 639-1 two-letter or ISO
* 639-2 three-letter code.
* @param preferredAudioLanguage Preferred audio language as an IETF BCP 47 conformant tag.
* @return This builder.
*/
public Builder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage) {
@ -73,8 +72,7 @@ public class TrackSelectionParameters implements Parcelable {
/**
* See {@link TrackSelectionParameters#preferredTextLanguage}.
*
* @param preferredTextLanguage Preferred text language as an ISO 639-1 two-letter or ISO 639-2
* three-letter code.
* @param preferredTextLanguage Preferred text language as an IETF BCP 47 conformant tag.
* @return This builder.
*/
public Builder setPreferredTextLanguage(@Nullable String preferredTextLanguage) {

View File

@ -419,16 +419,26 @@ public final class Util {
}
/**
* Returns a normalized ISO 639-2/T code for {@code language}.
* Returns a normalized IETF BCP 47 language tag for {@code language}.
*
* @param language A case-insensitive ISO 639-1 two-letter or ISO 639-2 three-letter language
* code.
* @param language A case-insensitive language code supported by {@link
* Locale#forLanguageTag(String)}.
* @return The all-lowercase normalized code, or null if the input was null, or {@code
* language.toLowerCase()} if the language could not be normalized.
*/
public static @Nullable String normalizeLanguageCode(@Nullable String language) {
if (language == null) {
return null;
}
try {
return language == null ? null : new Locale(language).getISO3Language();
Locale locale = Util.SDK_INT >= 21 ? Locale.forLanguageTag(language) : new Locale(language);
int localeLanguageLength = locale.getLanguage().length();
String normLanguage = locale.getISO3Language();
if (normLanguage.isEmpty()) {
return toLowerInvariant(language);
}
String normTag = Util.SDK_INT >= 21 ? locale.toLanguageTag() : locale.toString();
return toLowerInvariant(normLanguage + normTag.substring(localeLanguageLength));
} catch (MissingResourceException e) {
return toLowerInvariant(language);
}

View File

@ -930,7 +930,7 @@ public final class DefaultTrackSelectorTest {
// matches the preferred audio language.
trackGroups = wrapFormats(forcedDefault, forcedOnly, defaultOnly, noFlag, forcedOnlySpanish);
trackSelector.setParameters(
trackSelector.getParameters().buildUpon().setPreferredAudioLanguage("spa").build());
trackSelector.getParameters().buildUpon().setPreferredTextLanguage("spa").build());
result = trackSelector.selectTracks(textRendererCapabilities, trackGroups, periodId, TIMELINE);
assertFixedSelection(result.selections.get(0), trackGroups, forcedOnlySpanish);

View File

@ -34,6 +34,7 @@ import java.util.Random;
import java.util.zip.Deflater;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
/** Unit tests for {@link Util}. */
@RunWith(AndroidJUnit4.class)
@ -264,6 +265,31 @@ public class UtilTest {
assertThat(Arrays.copyOf(output.data, output.limit())).isEqualTo(testData);
}
@Test
@Config(sdk = 21)
public void testNormalizeLanguageCodeV21() {
assertThat(Util.normalizeLanguageCode("es")).isEqualTo("spa");
assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("spa");
assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("spa-ar");
assertThat(Util.normalizeLanguageCode("SpA-ar")).isEqualTo("spa-ar");
assertThat(Util.normalizeLanguageCode("es-AR-dialect")).isEqualTo("spa-ar-dialect");
assertThat(Util.normalizeLanguageCode("es-419")).isEqualTo("spa-419");
assertThat(Util.normalizeLanguageCode("zh-hans-tw")).isEqualTo("zho-hans-tw");
assertThat(Util.normalizeLanguageCode("zh-tw-hans")).isEqualTo("zho-tw");
assertThat(Util.normalizeLanguageCode("und")).isEqualTo("und");
assertThat(Util.normalizeLanguageCode("DoesNotExist")).isEqualTo("doesnotexist");
}
@Test
@Config(sdk = 16)
public void testNormalizeLanguageCode() {
assertThat(Util.normalizeLanguageCode("es")).isEqualTo("spa");
assertThat(Util.normalizeLanguageCode("spa")).isEqualTo("spa");
assertThat(Util.normalizeLanguageCode("es-AR")).isEqualTo("es-ar");
assertThat(Util.normalizeLanguageCode("und")).isEqualTo("und");
assertThat(Util.normalizeLanguageCode("DoesNotExist")).isEqualTo("doesnotexist");
}
private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) {
assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName);
assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName);
@ -273,5 +299,4 @@ public class UtilTest {
String escapedFileName = Util.escapeFileName(fileName);
assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName);
}
}

View File

@ -100,7 +100,7 @@ public class DefaultTrackNameProvider implements TrackNameProvider {
private String buildLanguageString(String language) {
Locale locale = Util.SDK_INT >= 21 ? Locale.forLanguageTag(language) : new Locale(language);
return locale.getDisplayLanguage();
return locale.getDisplayName();
}
private String joinWithSeparator(String... items) {