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. deprecated.
* Prevent seeking when ICY metadata is present to prevent playback problems * Prevent seeking when ICY metadata is present to prevent playback problems
([#5658](https://github.com/google/ExoPlayer/issues/5658)). ([#5658](https://github.com/google/ExoPlayer/issues/5658)).
* Use full BCP 47 language tags in `Format`.
### 2.9.6 ### ### 2.9.6 ###

View File

@ -156,7 +156,7 @@ public final class Format implements Parcelable {
// Audio and text specific. // 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; public final @Nullable String language;
/** /**
* The Accessibility channel, or {@link #NO_VALUE} if not known or applicable. * 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. * 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 * @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) { public void addAudioLanguagesToSelection(String... languages) {
assertPreparedWithMedia(); assertPreparedWithMedia();
@ -524,7 +524,7 @@ public final class DownloadHelper {
* selected for downloading if no track with one of the specified {@code languages} is * selected for downloading if no track with one of the specified {@code languages} is
* available. * available.
* @param languages A list of text languages for which tracks should be added to the download * @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( public void addTextLanguagesToSelection(
boolean selectUndeterminedTextLanguage, String... languages) { boolean selectUndeterminedTextLanguage, String... languages) {

View File

@ -2017,28 +2017,24 @@ public class DefaultTrackSelector extends MappingTrackSelector {
boolean isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; boolean isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0;
int trackScore; int trackScore;
boolean preferredLanguageFound = formatHasLanguage(format, params.preferredTextLanguage); int languageScore = getFormatLanguageScore(format, params.preferredTextLanguage);
if (preferredLanguageFound if (languageScore > 0
|| (params.selectUndeterminedTextLanguage && formatHasNoLanguage(format))) { || (params.selectUndeterminedTextLanguage && formatHasNoLanguage(format))) {
if (isDefault) { if (isDefault) {
trackScore = 8; trackScore = 11;
} else if (!isForced) { } else if (!isForced) {
// Prefer non-forced to forced if a preferred text language has been specified. Where // 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 // both are provided the non-forced track will usually contain the forced subtitles as
// a subset. // a subset.
trackScore = 6; trackScore = 8;
} else { } else {
trackScore = 4; trackScore = 5;
} }
trackScore += preferredLanguageFound ? 1 : 0; trackScore += languageScore;
} else if (isDefault) { } else if (isDefault) {
trackScore = 3; trackScore = 4;
} else if (isForced) { } else if (isForced) {
if (formatHasLanguage(format, params.preferredAudioLanguage)) { trackScore = 1 + languageScore;
trackScore = 2;
} else {
trackScore = 1;
}
} else { } else {
// Track should not be selected. // Track should not be selected.
continue; continue;
@ -2234,20 +2230,26 @@ public class DefaultTrackSelector extends MappingTrackSelector {
* @return Whether the {@link Format} does not define a language. * @return Whether the {@link Format} does not define a language.
*/ */
protected static boolean formatHasNoLanguage(Format format) { 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 * Returns a score for how well a language specified in a {@link Format} fits a given language.
* language} is null.
* *
* @param format The {@link Format}. * @param format The {@link Format}.
* @param language The language. * @param language The language, or null.
* @return Whether the format specifies the language, or {@code false} if {@code language} is * @return A score of 0 if the languages don't fit, a score of 1 if the languages fit partly and a
* null. * score of 2 if the languages fit fully.
*/ */
protected static boolean formatHasLanguage(Format format, @Nullable String language) { protected static int getFormatLanguageScore(Format format, @Nullable String language) {
return language != null && TextUtils.equals(language, format.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, 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) { public AudioTrackScore(Format format, Parameters parameters, int formatSupport) {
this.parameters = parameters; this.parameters = parameters;
withinRendererCapabilitiesScore = isSupported(formatSupport, false) ? 1 : 0; 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; defaultSelectionFlagScore = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0 ? 1 : 0;
channelCount = format.channelCount; channelCount = format.channelCount;
sampleRate = format.sampleRate; sampleRate = format.sampleRate;

View File

@ -59,8 +59,7 @@ public class TrackSelectionParameters implements Parcelable {
/** /**
* See {@link TrackSelectionParameters#preferredAudioLanguage}. * See {@link TrackSelectionParameters#preferredAudioLanguage}.
* *
* @param preferredAudioLanguage Preferred audio language as an ISO 639-1 two-letter or ISO * @param preferredAudioLanguage Preferred audio language as an IETF BCP 47 conformant tag.
* 639-2 three-letter code.
* @return This builder. * @return This builder.
*/ */
public Builder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage) { public Builder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage) {
@ -73,8 +72,7 @@ public class TrackSelectionParameters implements Parcelable {
/** /**
* See {@link TrackSelectionParameters#preferredTextLanguage}. * See {@link TrackSelectionParameters#preferredTextLanguage}.
* *
* @param preferredTextLanguage Preferred text language as an ISO 639-1 two-letter or ISO 639-2 * @param preferredTextLanguage Preferred text language as an IETF BCP 47 conformant tag.
* three-letter code.
* @return This builder. * @return This builder.
*/ */
public Builder setPreferredTextLanguage(@Nullable String preferredTextLanguage) { 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 * @param language A case-insensitive language code supported by {@link
* code. * Locale#forLanguageTag(String)}.
* @return The all-lowercase normalized code, or null if the input was null, or {@code * @return The all-lowercase normalized code, or null if the input was null, or {@code
* language.toLowerCase()} if the language could not be normalized. * language.toLowerCase()} if the language could not be normalized.
*/ */
public static @Nullable String normalizeLanguageCode(@Nullable String language) { public static @Nullable String normalizeLanguageCode(@Nullable String language) {
if (language == null) {
return null;
}
try { 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) { } catch (MissingResourceException e) {
return toLowerInvariant(language); return toLowerInvariant(language);
} }

View File

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

View File

@ -34,6 +34,7 @@ import java.util.Random;
import java.util.zip.Deflater; import java.util.zip.Deflater;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
/** Unit tests for {@link Util}. */ /** Unit tests for {@link Util}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@ -264,6 +265,31 @@ public class UtilTest {
assertThat(Arrays.copyOf(output.data, output.limit())).isEqualTo(testData); 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) { private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) {
assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName); assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName);
assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName);
@ -273,5 +299,4 @@ public class UtilTest {
String escapedFileName = Util.escapeFileName(fileName); String escapedFileName = Util.escapeFileName(fileName);
assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName);
} }
} }

View File

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