diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1defae4356..24262cb949 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -123,6 +123,8 @@ * Stop parsing unsupported WebVTT CSS properties. The spec provides an [exhaustive list](https://www.w3.org/TR/webvtt1/#the-cue-pseudo-element) of which are supported. + * Ignore excess characters in CEA-608 lines (max length is 32) + ([#7341](https://github.com/google/ExoPlayer/issues/7341)). * DRM: * Add support for attaching DRM sessions to clear content in the demo app. * Remove `DrmSessionManager` references from all renderers. @@ -188,11 +190,10 @@ * IMA extension: Upgrade to IMA SDK version 3.18.2, and migrate to new preloading APIs ([#6429](https://github.com/google/ExoPlayer/issues/6429)). * IMA extension: - * Upgrade to IMA SDK version 3.19.0, and migrate to new - preloading APIs - ([#6429](https://github.com/google/ExoPlayer/issues/6429)). - * Add support for timing out ad preloading, to avoid playback getting - stuck if an ad group unexpectedly fails to load. + * Upgrade to IMA SDK version 3.19.0, and migrate to new preloading APIs + ([#6429](https://github.com/google/ExoPlayer/issues/6429)). + * Add support for timing out ad preloading, to avoid playback getting + stuck if an ad group unexpectedly fails to load. * OkHttp extension: Upgrade OkHttp dependency to 3.12.11. * Cronet extension: Default to using the Cronet implementation in Google Play Services rather than Cronet Embedded. This allows Cronet to be used with a diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java index b1c554cf88..e7b0c5cb7d 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -1291,6 +1291,36 @@ public final class Util { return (toUnsignedLong(mostSignificantBits) << 32) | toUnsignedLong(leastSignificantBits); } + /** + * Truncates a sequence of ASCII characters to a maximum length. + * + *

Note: This is not safe to use in general on Unicode text because it may separate + * characters from combining characters or split up surrogate pairs. + * + * @param sequence The character sequence to truncate. + * @param maxLength The max length to truncate to. + * @return {@code sequence} directly if {@code sequence.length() <= maxLength}, otherwise {@code + * sequence.subsequence(0, maxLength}. + */ + public static CharSequence truncateAscii(CharSequence sequence, int maxLength) { + return sequence.length() <= maxLength ? sequence : sequence.subSequence(0, maxLength); + } + + /** + * Truncates a string of ASCII characters to a maximum length. + * + *

Note: This is not safe to use in general on Unicode text because it may separate + * characters from combining characters or split up surrogate pairs. + * + * @param string The string to truncate. + * @param maxLength The max length to truncate to. + * @return {@code string} directly if {@code string.length() <= maxLength}, otherwise {@code + * string.substring(0, maxLength}. + */ + public static String truncateAscii(String string, int maxLength) { + return string.length() <= maxLength ? string : string.substring(0, maxLength); + } + /** * Returns a byte array containing values parsed from the hex string provided. * diff --git a/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index 2e523a32c6..861267fc3a 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -24,9 +24,14 @@ import static com.google.android.exoplayer2.util.Util.parseXsDuration; import static com.google.android.exoplayer2.util.Util.unescapeFileName; import static com.google.common.truth.Truth.assertThat; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.StrikethroughSpan; +import android.text.style.UnderlineSpan; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.truth.SpannedSubject; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; @@ -713,6 +718,40 @@ public class UtilTest { assertThat(Util.toLong(0xFEDCBA, 0x87654321)).isEqualTo(0xFEDCBA_87654321L); } + @Test + public void truncateAscii_shortInput_returnsInput() { + String input = "a short string"; + + assertThat(Util.truncateAscii(input, 100)).isSameInstanceAs(input); + assertThat(Util.truncateAscii((CharSequence) input, 100)).isSameInstanceAs(input); + } + + @Test + public void truncateAscii_longInput_truncated() { + String input = "a much longer string"; + + assertThat(Util.truncateAscii(input, 5)).isEqualTo("a muc"); + assertThat(Util.truncateAscii((CharSequence) input, 5).toString()).isEqualTo("a muc"); + } + + @Test + public void truncateAscii_preservesStylingSpans() { + SpannableString input = new SpannableString("a short string"); + input.setSpan(new UnderlineSpan(), 0, 10, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + input.setSpan(new StrikethroughSpan(), 4, 10, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + CharSequence result = Util.truncateAscii(input, 7); + + assertThat(result).isInstanceOf(SpannableString.class); + assertThat(result.toString()).isEqualTo("a short"); + SpannedSubject.assertThat((Spanned) result) + .hasUnderlineSpanBetween(0, 7) + .withFlags(Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + SpannedSubject.assertThat((Spanned) result) + .hasStrikethroughSpanBetween(4, 7) + .withFlags(Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + } + @Test public void toHexString_returnsHexString() { byte[] bytes = TestUtil.createByteArray(0x12, 0xFC, 0x06); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 75e86c4113..dcb3d96841 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -37,6 +37,7 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; @@ -868,7 +869,11 @@ public final class Cea608Decoder extends CeaDecoder { } public void append(char text) { - captionStringBuilder.append(text); + // Don't accept more than 32 chars. We'll trim further, considering indent & tabOffset, in + // build(). + if (captionStringBuilder.length() < SCREEN_CHARWIDTH) { + captionStringBuilder.append(text); + } } public void rollUp() { @@ -883,14 +888,17 @@ public final class Cea608Decoder extends CeaDecoder { @Nullable public Cue build(@Cue.AnchorType int forcedPositionAnchor) { + // The number of empty columns before the start of the text, in the range [0-31]. + int startPadding = indent + tabOffset; + int maxTextLength = SCREEN_CHARWIDTH - startPadding; SpannableStringBuilder cueString = new SpannableStringBuilder(); // Add any rolled up captions, separated by new lines. for (int i = 0; i < rolledUpCaptions.size(); i++) { - cueString.append(rolledUpCaptions.get(i)); + cueString.append(Util.truncateAscii(rolledUpCaptions.get(i), maxTextLength)); cueString.append('\n'); } // Add the current line. - cueString.append(buildCurrentLine()); + cueString.append(Util.truncateAscii(buildCurrentLine(), maxTextLength)); if (cueString.length() == 0) { // The cue is empty. @@ -898,8 +906,7 @@ public final class Cea608Decoder extends CeaDecoder { } int positionAnchor; - // The number of empty columns before the start of the text, in the range [0-31]. - int startPadding = indent + tabOffset; + // The number of empty columns after the end of the text, in the same range. int endPadding = SCREEN_CHARWIDTH - startPadding - cueString.length(); int startEndPaddingDelta = startPadding - endPadding;