From e5aed73fbaf9a30900dd9f13a714d288d8dc68a7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 22 Mar 2019 13:13:34 +0000 Subject: [PATCH] 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 --- RELEASENOTES.md | 1 + .../com/google/android/exoplayer2/Format.java | 2 +- .../exoplayer2/offline/DownloadHelper.java | 4 +- .../trackselection/DefaultTrackSelector.java | 44 ++++++++++--------- .../TrackSelectionParameters.java | 6 +-- .../google/android/exoplayer2/util/Util.java | 18 ++++++-- .../DefaultTrackSelectorTest.java | 2 +- .../android/exoplayer2/util/UtilTest.java | 27 +++++++++++- .../ui/DefaultTrackNameProvider.java | 2 +- 9 files changed, 71 insertions(+), 35 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 398c041111..563d0a275b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index 140da38337..e170faf50b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -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. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 2c81ae1840..ae22200329 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -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) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index df8b705de5..0b510ea496 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -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 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; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java index 70572e6660..66a4707496 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java @@ -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) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 489314cbc8..d0f6766e34 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -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); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index 94ae1b5898..d2aad12f24 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -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); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index 8f4aad42a5..9abec0cd8f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -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); } - } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTrackNameProvider.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTrackNameProvider.java index 5d68387869..7034ba73f5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTrackNameProvider.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTrackNameProvider.java @@ -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) {