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("