diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f772bc9f19d..ae5bc0fb95d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -8,6 +8,9 @@ use this with `FfmpegAudioRenderer`. * Support extraction and decoding of Dolby Atmos ([#2465](https://github.com/google/ExoPlayer/issues/2465)). +* DefaultTrackSelector: Support undefined language text track selection when the + preferred language is not available + ([#2980](https://github.com/google/ExoPlayer/issues/2980)). ### 2.6.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 592589e2212..6a35c0c5e86 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -424,6 +424,11 @@ private C() {} */ public static final int SELECTION_FLAG_AUTOSELECT = 4; + /** + * Represents an undetermined language as an ISO 639 alpha-3 language code. + */ + public static final String LANGUAGE_UNDETERMINED = "und"; + /** * Represents a streaming or other media type. */ 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 c789caded4a..0029cdbd315 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 @@ -46,7 +46,7 @@ * Parameters currentParameters = trackSelector.getParameters(); * // Generate new parameters to prefer German audio and impose a maximum video size constraint. * Parameters newParameters = currentParameters - * .withPreferredAudioLanguage("de") + * .withPreferredAudioLanguage("deu") * .withMaxVideoSize(1024, 768); * // Set the new parameters on the selector. * trackSelector.setParameters(newParameters);} @@ -81,17 +81,22 @@ public static final class Parameters { // Audio /** - * The preferred language for audio, as well as for forced text tracks as defined by RFC 5646. + * The preferred language for audio, as well as for forced text tracks, as an ISO 639-2/T tag. * {@code null} selects the default track, or the first track if there's no default. */ public final String preferredAudioLanguage; // Text /** - * The preferred language for text tracks as defined by RFC 5646. {@code null} selects the + * The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the * default track if there is one, or no track otherwise. */ public final String preferredTextLanguage; + /** + * Whether a text track with undetermined language should be selected if no track with + * {@link #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. + */ + public final boolean selectUndeterminedTextLanguage; // Video /** @@ -150,6 +155,8 @@ public static final class Parameters { * */ public Parameters() { - this(null, null, false, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, - true, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); + this(null, null, false, false, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, + Integer.MAX_VALUE, true, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); } /** * @param preferredAudioLanguage See {@link #preferredAudioLanguage} * @param preferredTextLanguage See {@link #preferredTextLanguage} + * @param selectUndeterminedTextLanguage See {@link #selectUndeterminedTextLanguage}. * @param forceLowestBitrate See {@link #forceLowestBitrate}. * @param allowMixedMimeAdaptiveness See {@link #allowMixedMimeAdaptiveness} * @param allowNonSeamlessAdaptiveness See {@link #allowNonSeamlessAdaptiveness} @@ -181,13 +189,14 @@ public Parameters() { * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange} */ public Parameters(String preferredAudioLanguage, String preferredTextLanguage, - boolean forceLowestBitrate, boolean allowMixedMimeAdaptiveness, - boolean allowNonSeamlessAdaptiveness, int maxVideoWidth, int maxVideoHeight, - int maxVideoBitrate, boolean exceedVideoConstraintsIfNecessary, + boolean selectUndeterminedTextLanguage, boolean forceLowestBitrate, + boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness, int maxVideoWidth, + int maxVideoHeight, int maxVideoBitrate, boolean exceedVideoConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary, int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) { this.preferredAudioLanguage = preferredAudioLanguage; this.preferredTextLanguage = preferredTextLanguage; + this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; this.forceLowestBitrate = forceLowestBitrate; this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness; this.allowNonSeamlessAdaptiveness = allowNonSeamlessAdaptiveness; @@ -209,10 +218,11 @@ public Parameters withPreferredAudioLanguage(String preferredAudioLanguage) { if (TextUtils.equals(preferredAudioLanguage, this.preferredAudioLanguage)) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -223,10 +233,26 @@ public Parameters withPreferredTextLanguage(String preferredTextLanguage) { if (TextUtils.equals(preferredTextLanguage, this.preferredTextLanguage)) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); + } + + /** + * Returns an instance with the provided {@link #selectUndeterminedTextLanguage}. + */ + public Parameters withSelectUndeterminedTextLanguageAsFallback( + boolean selectUndeterminedTextLanguage) { + if (selectUndeterminedTextLanguage == this.selectUndeterminedTextLanguage) { + return this; + } + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -236,10 +262,11 @@ public Parameters withForceLowestBitrate(boolean forceLowestBitrate) { if (forceLowestBitrate == this.forceLowestBitrate) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -249,10 +276,11 @@ public Parameters withAllowMixedMimeAdaptiveness(boolean allowMixedMimeAdaptiven if (allowMixedMimeAdaptiveness == this.allowMixedMimeAdaptiveness) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -262,10 +290,11 @@ public Parameters withAllowNonSeamlessAdaptiveness(boolean allowNonSeamlessAdapt if (allowNonSeamlessAdaptiveness == this.allowNonSeamlessAdaptiveness) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -275,10 +304,11 @@ public Parameters withMaxVideoSize(int maxVideoWidth, int maxVideoHeight) { if (maxVideoWidth == this.maxVideoWidth && maxVideoHeight == this.maxVideoHeight) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -288,10 +318,11 @@ public Parameters withMaxVideoBitrate(int maxVideoBitrate) { if (maxVideoBitrate == this.maxVideoBitrate) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -320,10 +351,11 @@ public Parameters withExceedVideoConstraintsIfNecessary( if (exceedVideoConstraintsIfNecessary == this.exceedVideoConstraintsIfNecessary) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -334,10 +366,11 @@ public Parameters withExceedRendererCapabilitiesIfNecessary( if (exceedRendererCapabilitiesIfNecessary == this.exceedRendererCapabilitiesIfNecessary) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -350,10 +383,11 @@ public Parameters withViewportSize(int viewportWidth, int viewportHeight, && viewportOrientationMayChange == this.viewportOrientationMayChange) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -880,17 +914,20 @@ protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatS boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; boolean isForced = (format.selectionFlags & C.SELECTION_FLAG_FORCED) != 0; int trackScore; - if (formatHasLanguage(format, params.preferredTextLanguage)) { + boolean preferredLanguageFound = formatHasLanguage(format, params.preferredTextLanguage); + if (preferredLanguageFound + || (params.selectUndeterminedTextLanguage && formatHasNoLanguage(format))) { if (isDefault) { - trackScore = 6; + trackScore = 8; } 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 = 5; + trackScore = 6; } else { trackScore = 4; } + trackScore += preferredLanguageFound ? 1 : 0; } else if (isDefault) { trackScore = 3; } else if (isForced) { @@ -980,6 +1017,16 @@ protected static boolean isSupported(int formatSupport, boolean allowExceedsCapa && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } + /** + * Returns whether a {@link Format} does not define a language. + * + * @param format The {@link Format}. + * @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); + } + /** * Returns whether a {@link Format} specifies a particular language, or {@code false} if * {@code language} is null. 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 5b2de1042e3..24c5f4036de 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 @@ -50,6 +50,7 @@ import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; +import java.util.MissingResourceException; import java.util.TimeZone; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -238,13 +239,18 @@ public static void closeQuietly(Closeable closeable) { } /** - * Returns a normalized RFC 5646 language code. + * Returns a normalized RFC 639-2/T code for {@code language}. * - * @param language A possibly non-normalized RFC 5646 language code. - * @return The normalized code, or null if the input was null. + * @param language A case-insensitive ISO 639 alpha-2 or alpha-3 language 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. */ public static String normalizeLanguageCode(String language) { - return language == null ? null : new Locale(language).getLanguage(); + try { + return language == null ? null : new Locale(language).getISO3Language(); + } catch (MissingResourceException e) { + return language.toLowerCase(); + } } /** 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 a0e499139c9..b2b149b004d 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 @@ -36,6 +36,8 @@ public final class DefaultTrackSelectorTest { private static final Parameters DEFAULT_PARAMETERS = new Parameters(); private static final RendererCapabilities ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO); + private static final RendererCapabilities ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = + new FakeRendererCapabilities(C.TRACK_TYPE_TEXT); private static final RendererCapabilities ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO, FORMAT_EXCEEDS_CAPABILITIES); @@ -534,6 +536,60 @@ public void testSelectTracksExceedingCapabilitiesPreferLowerSampleRateBeforeBitr .isEqualTo(lowerSampleRateHigherBitrateFormat); } + /** + * Tests that the default track selector will select a text track with undetermined language if no + * text track with the preferred language is available but + * {@link Parameters#selectUndeterminedTextLanguage} is true. + */ + @Test + public void testSelectUndeterminedTextLanguageAsFallback() throws ExoPlaybackException{ + Format spanish = Format.createTextContainerFormat("spanish", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "spa"); + Format german = Format.createTextContainerFormat("german", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "de"); + Format undeterminedUnd = Format.createTextContainerFormat("undeterminedUnd", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "und"); + Format undeterminedNull = Format.createTextContainerFormat("undeterminedNull", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, null); + + RendererCapabilities[] textRendererCapabilites = + new RendererCapabilities[] {ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}; + + TrackSelectorResult result; + + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0)).isNull(); + + trackSelector.setParameters( + DEFAULT_PARAMETERS.withSelectUndeterminedTextLanguageAsFallback(true)); + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); + + trackSelector.setParameters(DEFAULT_PARAMETERS.withPreferredTextLanguage("spa")); + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(spanish); + + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0)).isNull(); + + trackSelector.setParameters( + trackSelector.getParameters().withSelectUndeterminedTextLanguageAsFallback(true)); + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); + + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(german, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedNull); + + result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(german)); + assertThat(result.selections.get(0)).isNull(); + } + /** * Tests that track selector will select audio tracks with lower bitrate when {@link Parameters} * indicate lowest bitrate preference, even when tracks are within capabilities. @@ -562,6 +618,14 @@ private static TrackGroupArray singleTrackGroup(Format... formats) { return new TrackGroupArray(new TrackGroup(formats)); } + private static TrackGroupArray wrapFormats(Format... formats) { + TrackGroup[] trackGroups = new TrackGroup[formats.length]; + for (int i = 0; i < trackGroups.length; i++) { + trackGroups[i] = new TrackGroup(formats[i]); + } + return new TrackGroupArray(trackGroups); + } + /** * A {@link RendererCapabilities} that advertises support for all formats of a given type using * a provided support value. For any format that does not have the given track type,