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) {