From 8c7d6447c0ca3d2de8b085cbc46a5a72fcbbf1d8 Mon Sep 17 00:00:00 2001 From: dlafayet Date: Fri, 30 Apr 2021 12:16:18 +0100 Subject: [PATCH] Merge #8858: Support ebutts:multiRowAlign in TTML text renderer Imported from GitHub PR https://github.com/google/ExoPlayer/pull/8858 Fix bug in text alignment inheritance where child does not correctly inherit ancestor's setting @icbaker Merge 70eb4bceb73b3f07e2f8d545b4fa7961189ac52a into 45616f916b28c9187b3e0f0dd18797464a079cdc COPYBARA_INTEGRATE_REVIEW=https://github.com/google/ExoPlayer/pull/8877 from dlafayet:multirowalign-cue d942b50a40525fea5d11b35a33d3bbc512550960 PiperOrigin-RevId: 371306966 --- RELEASENOTES.md | 1 + .../google/android/exoplayer2/text/Cue.java | 26 ++++++++++++++ .../exoplayer2/text/ttml/TtmlDecoder.java | 36 ++++++++++--------- .../exoplayer2/text/ttml/TtmlNode.java | 5 ++- .../exoplayer2/text/ttml/TtmlStyle.java | 14 ++++++++ .../android/exoplayer2/text/CueTest.java | 3 ++ .../exoplayer2/text/ttml/TtmlDecoderTest.java | 27 ++++++++++++++ .../exoplayer2/text/ttml/TtmlStyleTest.java | 15 ++++++++ .../exoplayer2/ui/WebViewSubtitleOutput.java | 19 +++++++--- .../assets/media/ttml/multi_row_align.xml | 34 ++++++++++++++++++ 10 files changed, 159 insertions(+), 21 deletions(-) create mode 100644 testdata/src/test/assets/media/ttml/multi_row_align.xml diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 345c4f1fae..4d1ac016b4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -131,6 +131,7 @@ ([#8435](https://github.com/google/ExoPlayer/issues/8435)). * Ensure TTML `tts:textAlign` is correctly propagated from `

` nodes to child nodes. + * Support TTML `ebutts:multiRowAlign` attributes. * MediaSession extension: Remove dependency to core module and rely on common only. The `TimelineQueueEditor` uses a new `MediaDescriptionConverter` for this purpose and does not rely on the `ConcatenatingMediaSource` anymore. diff --git a/library/common/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/common/src/main/java/com/google/android/exoplayer2/text/Cue.java index 49a45e1b22..46f865782f 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -140,6 +140,12 @@ public final class Cue { /** The alignment of the cue text within the cue box, or null if the alignment is undefined. */ @Nullable public final Alignment textAlignment; + /** + * The alignment of multiple lines of text relative to the longest line, or null if the alignment + * is undefined. + */ + @Nullable public final Alignment multiRowAlignment; + /** The cue image, or null if this is a text cue. */ @Nullable public final Bitmap bitmap; @@ -364,6 +370,7 @@ public final class Cue { this( text, textAlignment, + /* multiRowAlignment= */ null, /* bitmap= */ null, line, lineType, @@ -410,6 +417,7 @@ public final class Cue { this( text, textAlignment, + /* multiRowAlignment= */ null, /* bitmap= */ null, line, lineType, @@ -429,6 +437,7 @@ public final class Cue { private Cue( @Nullable CharSequence text, @Nullable Alignment textAlignment, + @Nullable Alignment multiRowAlignment, @Nullable Bitmap bitmap, float line, @LineType int lineType, @@ -451,6 +460,7 @@ public final class Cue { } this.text = text; this.textAlignment = textAlignment; + this.multiRowAlignment = multiRowAlignment; this.bitmap = bitmap; this.line = line; this.lineType = lineType; @@ -477,6 +487,7 @@ public final class Cue { @Nullable private CharSequence text; @Nullable private Bitmap bitmap; @Nullable private Alignment textAlignment; + @Nullable private Alignment multiRowAlignment; private float line; @LineType private int lineType; @AnchorType private int lineAnchor; @@ -495,6 +506,7 @@ public final class Cue { text = null; bitmap = null; textAlignment = null; + multiRowAlignment = null; line = DIMEN_UNSET; lineType = TYPE_UNSET; lineAnchor = TYPE_UNSET; @@ -513,6 +525,7 @@ public final class Cue { text = cue.text; bitmap = cue.bitmap; textAlignment = cue.textAlignment; + multiRowAlignment = cue.multiRowAlignment; line = cue.line; lineType = cue.lineType; lineAnchor = cue.lineAnchor; @@ -592,6 +605,18 @@ public final class Cue { return textAlignment; } + /** + * Sets the multi-row alignment of the cue. + * + *

Passing null means the alignment is undefined. + * + * @see Cue#multiRowAlignment + */ + public Builder setMultiRowAlignment(@Nullable Layout.Alignment multiRowAlignment) { + this.multiRowAlignment = multiRowAlignment; + return this; + } + /** * Sets the position of the cue box within the viewport in the direction orthogonal to the * writing direction. @@ -827,6 +852,7 @@ public final class Cue { return new Cue( text, textAlignment, + multiRowAlignment, bitmap, line, lineType, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java index f06ca59546..1d84b7a6b3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java @@ -534,22 +534,10 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { TtmlNode.ITALIC.equalsIgnoreCase(attributeValue)); break; case TtmlNode.ATTR_TTS_TEXT_ALIGN: - switch (Ascii.toLowerCase(attributeValue)) { - case TtmlNode.LEFT: - case TtmlNode.START: - style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_NORMAL); - break; - case TtmlNode.RIGHT: - case TtmlNode.END: - style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_OPPOSITE); - break; - case TtmlNode.CENTER: - style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_CENTER); - break; - default: - // ignore - break; - } + style = createIfNull(style).setTextAlign(parseAlignment(attributeValue)); + break; + case TtmlNode.ATTR_EBUTTS_MULTI_ROW_ALIGN: + style = createIfNull(style).setMultiRowAlign(parseAlignment(attributeValue)); break; case TtmlNode.ATTR_TTS_TEXT_COMBINE: switch (Ascii.toLowerCase(attributeValue)) { @@ -632,6 +620,22 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { return style == null ? new TtmlStyle() : style; } + @Nullable + private static Layout.Alignment parseAlignment(String alignment) { + switch (Ascii.toLowerCase(alignment)) { + case TtmlNode.LEFT: + case TtmlNode.START: + return Layout.Alignment.ALIGN_NORMAL; + case TtmlNode.RIGHT: + case TtmlNode.END: + return Layout.Alignment.ALIGN_OPPOSITE; + case TtmlNode.CENTER: + return Layout.Alignment.ALIGN_CENTER; + default: + return null; + } + } + private static TtmlNode parseNode( XmlPullParser parser, @Nullable TtmlNode parent, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java index a66c609a86..7e39d1e9f8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java @@ -72,6 +72,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public static final String ATTR_TTS_TEXT_EMPHASIS = "textEmphasis"; public static final String ATTR_TTS_WRITING_MODE = "writingMode"; public static final String ATTR_TTS_SHEAR = "shear"; + public static final String ATTR_EBUTTS_MULTI_ROW_ALIGN = "multiRowAlign"; // Values for ruby public static final String RUBY_CONTAINER = "container"; @@ -376,7 +377,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return; } String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId; - for (Map.Entry entry : nodeEndsByRegion.entrySet()) { String regionId = entry.getKey(); int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0; @@ -423,6 +423,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; if (resolvedStyle.getTextAlign() != null) { regionOutput.setTextAlignment(resolvedStyle.getTextAlign()); } + if (resolvedStyle.getMultiRowAlign() != null) { + regionOutput.setMultiRowAlignment(resolvedStyle.getMultiRowAlign()); + } } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java index d04fde3570..d1c7291652 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java @@ -86,6 +86,7 @@ import java.lang.annotation.RetentionPolicy; @RubyType private int rubyType; @TextAnnotation.Position private int rubyPosition; @Nullable private Layout.Alignment textAlign; + @Nullable private Layout.Alignment multiRowAlign; @OptionalBoolean private int textCombine; @Nullable private TextEmphasis textEmphasis; private float shearPercentage; @@ -244,6 +245,9 @@ import java.lang.annotation.RetentionPolicy; if (textAlign == null && ancestor.textAlign != null) { textAlign = ancestor.textAlign; } + if (multiRowAlign == null && ancestor.multiRowAlign != null) { + multiRowAlign = ancestor.multiRowAlign; + } if (textCombine == UNSPECIFIED) { textCombine = ancestor.textCombine; } @@ -308,6 +312,16 @@ import java.lang.annotation.RetentionPolicy; return this; } + @Nullable + public Layout.Alignment getMultiRowAlign() { + return multiRowAlign; + } + + public TtmlStyle setMultiRowAlign(@Nullable Layout.Alignment multiRowAlign) { + this.multiRowAlign = multiRowAlign; + return this; + } + /** Returns true if the source entity has {@code tts:textCombine=all}. */ public boolean getTextCombine() { return textCombine == ON; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java index 74d87e08b8..1d33c2834e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/CueTest.java @@ -37,6 +37,7 @@ public class CueTest { new Cue.Builder() .setText(SpannedString.valueOf("text")) .setTextAlignment(Layout.Alignment.ALIGN_CENTER) + .setMultiRowAlignment(Layout.Alignment.ALIGN_NORMAL) .setLine(5, Cue.LINE_TYPE_NUMBER) .setLineAnchor(Cue.ANCHOR_TYPE_END) .setPosition(0.4f) @@ -52,6 +53,7 @@ public class CueTest { assertThat(cue.text.toString()).isEqualTo("text"); assertThat(cue.textAlignment).isEqualTo(Layout.Alignment.ALIGN_CENTER); + assertThat(cue.multiRowAlignment).isEqualTo(Layout.Alignment.ALIGN_NORMAL); assertThat(cue.line).isEqualTo(5); assertThat(cue.lineType).isEqualTo(Cue.LINE_TYPE_NUMBER); assertThat(cue.position).isEqualTo(0.4f); @@ -66,6 +68,7 @@ public class CueTest { assertThat(modifiedCue.text).isSameInstanceAs(cue.text); assertThat(modifiedCue.textAlignment).isEqualTo(cue.textAlignment); + assertThat(modifiedCue.multiRowAlignment).isEqualTo(cue.multiRowAlignment); assertThat(modifiedCue.line).isEqualTo(cue.line); assertThat(modifiedCue.lineType).isEqualTo(cue.lineType); assertThat(modifiedCue.position).isEqualTo(cue.position); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java index 2b77ab7243..d068ade74b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java @@ -65,6 +65,7 @@ public final class TtmlDecoderTest { private static final String BITMAP_UNSUPPORTED_REGION_FILE = "media/ttml/bitmap_unsupported_region.xml"; private static final String TEXT_ALIGN_FILE = "media/ttml/text_align.xml"; + private static final String MULTI_ROW_ALIGN_FILE = "media/ttml/multi_row_align.xml"; private static final String VERTICAL_TEXT_FILE = "media/ttml/vertical_text.xml"; private static final String TEXT_COMBINE_FILE = "media/ttml/text_combine.xml"; private static final String RUBIES_FILE = "media/ttml/rubies.xml"; @@ -617,6 +618,32 @@ public final class TtmlDecoderTest { assertThat(ninthCue.textAlignment).isNull(); } + @Test + public void multiRowAlign() throws IOException, SubtitleDecoderException { + TtmlSubtitle subtitle = getSubtitle(MULTI_ROW_ALIGN_FILE); + + Cue firstCue = getOnlyCueAtTimeUs(subtitle, 10_000_000); + assertThat(firstCue.multiRowAlignment).isEqualTo(Layout.Alignment.ALIGN_NORMAL); + + Cue secondCue = getOnlyCueAtTimeUs(subtitle, 20_000_000); + assertThat(secondCue.multiRowAlignment).isEqualTo(Layout.Alignment.ALIGN_CENTER); + + Cue thirdCue = getOnlyCueAtTimeUs(subtitle, 30_000_000); + assertThat(thirdCue.multiRowAlignment).isEqualTo(Layout.Alignment.ALIGN_OPPOSITE); + + Cue fourthCue = getOnlyCueAtTimeUs(subtitle, 40_000_000); + assertThat(fourthCue.multiRowAlignment).isEqualTo(Layout.Alignment.ALIGN_NORMAL); + + Cue fifthCue = getOnlyCueAtTimeUs(subtitle, 50_000_000); + assertThat(fifthCue.multiRowAlignment).isEqualTo(Layout.Alignment.ALIGN_OPPOSITE); + + Cue sixthCue = getOnlyCueAtTimeUs(subtitle, 60_000_000); + assertThat(sixthCue.multiRowAlignment).isNull(); + + Cue seventhCue = getOnlyCueAtTimeUs(subtitle, 70_000_000); + assertThat(seventhCue.multiRowAlignment).isNull(); + } + @Test public void verticalText() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(VERTICAL_TEXT_FILE); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java index 4583701cc3..972d7876e2 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java @@ -66,6 +66,7 @@ public final class TtmlStyleTest { .setRubyType(RUBY_TYPE) .setRubyPosition(RUBY_POSITION) .setTextAlign(TEXT_ALIGN) + .setMultiRowAlign(Layout.Alignment.ALIGN_NORMAL) .setTextCombine(TEXT_COMBINE) .setTextEmphasis(TextEmphasis.parse(TEXT_EMPHASIS_STYLE)) .setShearPercentage(SHEAR_PERCENTAGE); @@ -85,6 +86,7 @@ public final class TtmlStyleTest { assertThat(style.getFontSizeUnit()).isEqualTo(FONT_SIZE_UNIT); assertThat(style.getRubyPosition()).isEqualTo(RUBY_POSITION); assertThat(style.getTextAlign()).isEqualTo(TEXT_ALIGN); + assertThat(style.getMultiRowAlign()).isEqualTo(Layout.Alignment.ALIGN_NORMAL); assertThat(style.getTextCombine()).isEqualTo(TEXT_COMBINE); assertWithMessage("rubyType should not be inherited") .that(style.getRubyType()) @@ -115,6 +117,7 @@ public final class TtmlStyleTest { assertThat(style.getFontSizeUnit()).isEqualTo(FONT_SIZE_UNIT); assertThat(style.getRubyPosition()).isEqualTo(RUBY_POSITION); assertThat(style.getTextAlign()).isEqualTo(TEXT_ALIGN); + assertThat(style.getMultiRowAlign()).isEqualTo(Layout.Alignment.ALIGN_NORMAL); assertThat(style.getTextCombine()).isEqualTo(TEXT_COMBINE); assertWithMessage("backgroundColor should be chained") .that(style.getBackgroundColor()) @@ -253,6 +256,18 @@ public final class TtmlStyleTest { assertThat(style.getTextAlign()).isNull(); } + @Test + public void multiRowAlign() { + TtmlStyle style = new TtmlStyle(); + assertThat(style.getMultiRowAlign()).isEqualTo(null); + style.setMultiRowAlign(Layout.Alignment.ALIGN_CENTER); + assertThat(style.getMultiRowAlign()).isEqualTo(Layout.Alignment.ALIGN_CENTER); + style.setMultiRowAlign(Layout.Alignment.ALIGN_NORMAL); + assertThat(style.getMultiRowAlign()).isEqualTo(Layout.Alignment.ALIGN_NORMAL); + style.setMultiRowAlign(Layout.Alignment.ALIGN_OPPOSITE); + assertThat(style.getMultiRowAlign()).isEqualTo(Layout.Alignment.ALIGN_OPPOSITE); + } + @Test public void textCombine() { TtmlStyle style = new TtmlStyle(); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/WebViewSubtitleOutput.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/WebViewSubtitleOutput.java index c57845da49..4e04bc521f 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/WebViewSubtitleOutput.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/WebViewSubtitleOutput.java @@ -302,11 +302,22 @@ import java.util.Map; horizontalTranslatePercent, verticalTranslatePercent, getBlockShearTransformFunction(cue))) - .append(Util.formatInvariant("", DEFAULT_BACKGROUND_CSS_CLASS)) - .append(htmlAndCss.html) - .append("") - .append(""); + .append(Util.formatInvariant("", DEFAULT_BACKGROUND_CSS_CLASS)); + + if (cue.multiRowAlignment != null) { + html.append( + Util.formatInvariant( + "", + convertAlignmentToCss(cue.multiRowAlignment))) + .append(htmlAndCss.html) + .append(""); + } else { + html.append(htmlAndCss.html); + } + + html.append("").append(""); } + html.append(""); StringBuilder htmlHead = new StringBuilder(); diff --git a/testdata/src/test/assets/media/ttml/multi_row_align.xml b/testdata/src/test/assets/media/ttml/multi_row_align.xml new file mode 100644 index 0000000000..1547b3f089 --- /dev/null +++ b/testdata/src/test/assets/media/ttml/multi_row_align.xml @@ -0,0 +1,34 @@ + + + + + + + +

+

multi row
align start

+
+
+

multi row
align center

+
+
+

multi row
align end

+
+
+

multi row
align left

+
+
+

multi row
align right

+
+
+

no multi row
align

+
+
+

align set on
span (invalid)

+
+ +