diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 320a4e7494..dd3c858c3e 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -49,6 +49,8 @@
* Catch-and-log all fatal exceptions in `TextRenderer` instead of re-throwing,
allowing playback to continue even if subtitles fail
([#6885](https://github.com/google/ExoPlayer/issues/6885)).
+ * Parse `tts:ruby` and `tts:rubyPosition` properties in TTML subtitles
+ (rendering is coming later).
* DRM:
* Add support for attaching DRM sessions to clear content in the demo app.
* Remove `DrmSessionManager` references from all renderers.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/DeleteTextSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/DeleteTextSpan.java
new file mode 100644
index 0000000000..be41c3957c
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/DeleteTextSpan.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package com.google.android.exoplayer2.text.ttml;
+
+import android.text.Spanned;
+
+/**
+ * A span used to mark a section of text for later deletion.
+ *
+ *
This is deliberately package-private because it's not generally supported by Android and
+ * results in surprising behaviour when simply calling {@link Spanned#toString} (i.e. the text isn't
+ * deleted).
+ *
+ *
This span is explicitly handled in {@code TtmlNode#cleanUpText}.
+ */
+/* package */ final class DeleteTextSpan {}
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 09df870bda..80009d4aac 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
@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
import com.google.android.exoplayer2.text.Subtitle;
import com.google.android.exoplayer2.text.SubtitleDecoderException;
+import com.google.android.exoplayer2.text.span.RubySpan;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ColorParser;
import com.google.android.exoplayer2.util.Log;
@@ -537,6 +538,40 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
break;
}
break;
+ case TtmlNode.ATTR_TTS_RUBY:
+ switch (Util.toLowerInvariant(attributeValue)) {
+ case TtmlNode.RUBY_CONTAINER:
+ style = createIfNull(style).setRubyType(TtmlStyle.RUBY_TYPE_CONTAINER);
+ break;
+ case TtmlNode.RUBY_BASE:
+ case TtmlNode.RUBY_BASE_CONTAINER:
+ style = createIfNull(style).setRubyType(TtmlStyle.RUBY_TYPE_BASE);
+ break;
+ case TtmlNode.RUBY_TEXT:
+ case TtmlNode.RUBY_TEXT_CONTAINER:
+ style = createIfNull(style).setRubyType(TtmlStyle.RUBY_TYPE_TEXT);
+ break;
+ case TtmlNode.RUBY_DELIMITER:
+ style = createIfNull(style).setRubyType(TtmlStyle.RUBY_TYPE_DELIMITER);
+ break;
+ default:
+ // ignore
+ break;
+ }
+ break;
+ case TtmlNode.ATTR_TTS_RUBY_POSITION:
+ switch (Util.toLowerInvariant(attributeValue)) {
+ case TtmlNode.RUBY_BEFORE:
+ style = createIfNull(style).setRubyPosition(RubySpan.POSITION_OVER);
+ break;
+ case TtmlNode.RUBY_AFTER:
+ style = createIfNull(style).setRubyPosition(RubySpan.POSITION_UNDER);
+ break;
+ default:
+ // ignore
+ break;
+ }
+ break;
case TtmlNode.ATTR_TTS_TEXT_DECORATION:
switch (Util.toLowerInvariant(attributeValue)) {
case TtmlNode.LINETHROUGH:
@@ -650,8 +685,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
endTime = parent.endTimeUs;
}
}
+
return TtmlNode.buildNode(
- parser.getName(), startTime, endTime, style, styleIds, regionId, imageId);
+ parser.getName(), startTime, endTime, style, styleIds, regionId, imageId, parent);
}
private static boolean isSupportedTag(String tag) {
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 df59639d82..c8e9ed7ce0 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
@@ -64,11 +64,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public static final String ATTR_TTS_FONT_FAMILY = "fontFamily";
public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight";
public static final String ATTR_TTS_COLOR = "color";
+ public static final String ATTR_TTS_RUBY = "ruby";
+ public static final String ATTR_TTS_RUBY_POSITION = "rubyPosition";
public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration";
public static final String ATTR_TTS_TEXT_ALIGN = "textAlign";
public static final String ATTR_TTS_TEXT_COMBINE = "textCombine";
public static final String ATTR_TTS_WRITING_MODE = "writingMode";
+ // Values for ruby
+ public static final String RUBY_CONTAINER = "container";
+ public static final String RUBY_BASE = "base";
+ public static final String RUBY_BASE_CONTAINER = "baseContainer";
+ public static final String RUBY_TEXT = "text";
+ public static final String RUBY_TEXT_CONTAINER = "textContainer";
+ public static final String RUBY_DELIMITER = "delimiter";
+
+ // Values for rubyPosition
+ public static final String RUBY_BEFORE = "before";
+ public static final String RUBY_AFTER = "after";
// Values for textDecoration
public static final String LINETHROUGH = "linethrough";
public static final String NO_LINETHROUGH = "nolinethrough";
@@ -102,6 +115,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Nullable private final String[] styleIds;
public final String regionId;
@Nullable public final String imageId;
+ @Nullable public final TtmlNode parent;
private final HashMap nodeStartsByRegion;
private final HashMap nodeEndsByRegion;
@@ -117,7 +131,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* style= */ null,
/* styleIds= */ null,
ANONYMOUS_REGION_ID,
- /* imageId= */ null);
+ /* imageId= */ null,
+ /* parent= */ null);
}
public static TtmlNode buildNode(
@@ -127,9 +142,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Nullable TtmlStyle style,
@Nullable String[] styleIds,
String regionId,
- @Nullable String imageId) {
+ @Nullable String imageId,
+ @Nullable TtmlNode parent) {
return new TtmlNode(
- tag, /* text= */ null, startTimeUs, endTimeUs, style, styleIds, regionId, imageId);
+ tag, /* text= */ null, startTimeUs, endTimeUs, style, styleIds, regionId, imageId, parent);
}
private TtmlNode(
@@ -140,7 +156,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Nullable TtmlStyle style,
@Nullable String[] styleIds,
String regionId,
- @Nullable String imageId) {
+ @Nullable String imageId,
+ @Nullable TtmlNode parent) {
this.tag = tag;
this.text = text;
this.imageId = imageId;
@@ -150,6 +167,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs;
this.regionId = Assertions.checkNotNull(regionId);
+ this.parent = parent;
nodeStartsByRegion = new HashMap<>();
nodeEndsByRegion = new HashMap<>();
}
@@ -361,14 +379,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
regionOutput.setText(text);
}
if (resolvedStyle != null) {
- TtmlRenderUtil.applyStylesToSpan(text, start, end, resolvedStyle);
+ TtmlRenderUtil.applyStylesToSpan(text, start, end, resolvedStyle, parent);
regionOutput.setVerticalType(resolvedStyle.getVerticalType());
}
}
private static void cleanUpText(SpannableStringBuilder builder) {
// Having joined the text elements, we need to do some final cleanup on the result.
- // 1. Collapse multiple consecutive spaces into a single space.
+ // Remove any text covered by a DeleteTextSpan (e.g. ruby text).
+ DeleteTextSpan[] deleteTextSpans = builder.getSpans(0, builder.length(), DeleteTextSpan.class);
+ for (DeleteTextSpan deleteTextSpan : deleteTextSpans) {
+ builder.replace(builder.getSpanStart(deleteTextSpan), builder.getSpanEnd(deleteTextSpan), "");
+ }
+ // Collapse multiple consecutive spaces into a single space.
for (int i = 0; i < builder.length(); i++) {
if (builder.charAt(i) == ' ') {
int j = i + 1;
@@ -381,7 +404,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
}
}
- // 2. Remove any spaces from the start of each line.
+ // Remove any spaces from the start of each line.
if (builder.length() > 0 && builder.charAt(0) == ' ') {
builder.delete(0, 1);
}
@@ -390,7 +413,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
builder.delete(i + 1, i + 2);
}
}
- // 3. Remove any spaces from the end of each line.
+ // Remove any spaces from the end of each line.
if (builder.length() > 0 && builder.charAt(builder.length() - 1) == ' ') {
builder.delete(builder.length() - 1, builder.length());
}
@@ -399,7 +422,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
builder.delete(i, i + 1);
}
}
- // 4. Trim a trailing newline, if there is one.
+ // Trim a trailing newline, if there is one.
if (builder.length() > 0 && builder.charAt(builder.length() - 1) == '\n') {
builder.delete(builder.length() - 1, builder.length());
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java
index f209936b39..e5ba2c9c1c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.text.ttml;
import android.text.Layout.Alignment;
+import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan;
@@ -29,7 +30,12 @@ import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
+import com.google.android.exoplayer2.text.span.RubySpan;
import com.google.android.exoplayer2.text.span.SpanUtil;
+import com.google.android.exoplayer2.util.Log;
+import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayDeque;
+import java.util.Deque;
import java.util.Map;
/**
@@ -37,6 +43,8 @@ import java.util.Map;
*/
/* package */ final class TtmlRenderUtil {
+ private static final String TAG = "TtmlRenderUtil";
+
@Nullable
public static TtmlStyle resolveStyle(
@Nullable TtmlStyle style, @Nullable String[] styleIds, Map globalStyles) {
@@ -71,8 +79,8 @@ import java.util.Map;
return style;
}
- public static void applyStylesToSpan(SpannableStringBuilder builder,
- int start, int end, TtmlStyle style) {
+ public static void applyStylesToSpan(
+ Spannable builder, int start, int end, TtmlStyle style, @Nullable TtmlNode parent) {
if (style.getStyle() != TtmlStyle.UNSPECIFIED) {
builder.setSpan(new StyleSpan(style.getStyle()), start, end,
@@ -108,6 +116,53 @@ import java.util.Map;
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
+ switch (style.getRubyType()) {
+ case TtmlStyle.RUBY_TYPE_BASE:
+ // look for the sibling RUBY_TEXT and add it as span between start & end.
+ @Nullable TtmlNode containerNode = findRubyContainerNode(parent);
+ if (containerNode == null) {
+ // No matching container node
+ break;
+ }
+ @Nullable TtmlNode textNode = findRubyTextNode(containerNode);
+ if (textNode == null) {
+ // no matching text node
+ break;
+ }
+ String rubyText;
+ if (textNode.getChildCount() == 1 && textNode.getChild(0).text != null) {
+ rubyText = Util.castNonNull(textNode.getChild(0).text);
+ } else {
+ Log.i(TAG, "Skipping rubyText node without exactly one text child.");
+ break;
+ }
+
+ // TODO: Get rubyPosition from `textNode` when TTML inheritance is implemented.
+ @RubySpan.Position
+ int rubyPosition =
+ containerNode.style != null
+ ? containerNode.style.getRubyPosition()
+ : RubySpan.POSITION_UNKNOWN;
+ builder.setSpan(
+ new RubySpan(rubyText, rubyPosition), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TtmlStyle.RUBY_TYPE_DELIMITER:
+ // TODO: Add support for this when RubySpan supports parenthetical text. For now, just
+ // fall through and delete the text.
+ case TtmlStyle.RUBY_TYPE_TEXT:
+ // We can't just remove the text directly from `builder` here because TtmlNode has fixed
+ // ideas of where every node starts and ends (nodeStartsByRegion and nodeEndsByRegion) so
+ // all these indices become invalid if we mutate the underlying string at this point.
+ // Instead we add a special span that's then handled in TtmlNode#cleanUpText.
+ builder.setSpan(new DeleteTextSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TtmlStyle.RUBY_TYPE_CONTAINER:
+ case TtmlStyle.UNSPECIFIED:
+ default:
+ // Do nothing
+ break;
+ }
+
@Nullable Alignment textAlign = style.getTextAlign();
if (textAlign != null) {
SpanUtil.addOrReplaceSpan(
@@ -156,6 +211,35 @@ import java.util.Map;
}
}
+ @Nullable
+ private static TtmlNode findRubyTextNode(TtmlNode rubyContainerNode) {
+ Deque childNodesStack = new ArrayDeque<>();
+ childNodesStack.push(rubyContainerNode);
+ while (!childNodesStack.isEmpty()) {
+ TtmlNode childNode = childNodesStack.pop();
+ if (childNode.style != null && childNode.style.getRubyType() == TtmlStyle.RUBY_TYPE_TEXT) {
+ return childNode;
+ }
+ for (int i = childNode.getChildCount() - 1; i >= 0; i--) {
+ childNodesStack.push(childNode.getChild(i));
+ }
+ }
+
+ return null;
+ }
+
+ @Nullable
+ private static TtmlNode findRubyContainerNode(@Nullable TtmlNode node) {
+ while (node != null) {
+ @Nullable TtmlStyle style = node.style;
+ if (style != null && style.getRubyType() == TtmlStyle.RUBY_TYPE_CONTAINER) {
+ return node;
+ }
+ node = node.parent;
+ }
+ return null;
+ }
+
/**
* Called when the end of a paragraph is encountered. Adds a newline if there are one or more
* non-space characters since the previous newline.
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 929c4c02fc..928af3620c 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
@@ -21,6 +21,7 @@ import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Cue.VerticalType;
+import com.google.android.exoplayer2.text.span.RubySpan;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -61,6 +62,16 @@ import java.lang.annotation.RetentionPolicy;
private static final int OFF = 0;
private static final int ON = 1;
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({UNSPECIFIED, RUBY_TYPE_CONTAINER, RUBY_TYPE_BASE, RUBY_TYPE_TEXT, RUBY_TYPE_DELIMITER})
+ public @interface RubyType {}
+
+ public static final int RUBY_TYPE_CONTAINER = 1;
+ public static final int RUBY_TYPE_BASE = 2;
+ public static final int RUBY_TYPE_TEXT = 3;
+ public static final int RUBY_TYPE_DELIMITER = 4;
+
@Nullable private String fontFamily;
private int fontColor;
private boolean hasFontColor;
@@ -73,6 +84,8 @@ import java.lang.annotation.RetentionPolicy;
@FontSizeUnit private int fontSizeUnit;
private float fontSize;
@Nullable private String id;
+ @RubyType private int rubyType;
+ @RubySpan.Position private int rubyPosition;
@Nullable private Layout.Alignment textAlign;
@OptionalBoolean private int textCombine;
@Cue.VerticalType private int verticalType;
@@ -83,6 +96,8 @@ import java.lang.annotation.RetentionPolicy;
bold = UNSPECIFIED;
italic = UNSPECIFIED;
fontSizeUnit = UNSPECIFIED;
+ rubyType = UNSPECIFIED;
+ rubyPosition = RubySpan.POSITION_UNKNOWN;
textCombine = UNSPECIFIED;
verticalType = Cue.TYPE_UNSET;
}
@@ -214,6 +229,9 @@ import java.lang.annotation.RetentionPolicy;
if (underline == UNSPECIFIED) {
underline = ancestor.underline;
}
+ if (rubyPosition == RubySpan.POSITION_UNKNOWN) {
+ rubyPosition = ancestor.rubyPosition;
+ }
if (textAlign == null && ancestor.textAlign != null) {
textAlign = ancestor.textAlign;
}
@@ -228,8 +246,11 @@ import java.lang.annotation.RetentionPolicy;
if (chaining && !hasBackgroundColor && ancestor.hasBackgroundColor) {
setBackgroundColor(ancestor.backgroundColor);
}
- if (chaining && verticalType == Cue.TYPE_UNSET) {
- verticalType = ancestor.verticalType;
+ if (chaining && rubyType == UNSPECIFIED && ancestor.rubyType != UNSPECIFIED) {
+ rubyType = ancestor.rubyType;
+ }
+ if (chaining && verticalType == Cue.TYPE_UNSET && ancestor.verticalType != Cue.TYPE_UNSET) {
+ setVerticalType(ancestor.verticalType);
}
}
return this;
@@ -245,6 +266,26 @@ import java.lang.annotation.RetentionPolicy;
return id;
}
+ public TtmlStyle setRubyType(@RubyType int rubyType) {
+ this.rubyType = rubyType;
+ return this;
+ }
+
+ @RubyType
+ public int getRubyType() {
+ return rubyType;
+ }
+
+ public TtmlStyle setRubyPosition(@RubySpan.Position int position) {
+ this.rubyPosition = position;
+ return this;
+ }
+
+ @RubySpan.Position
+ public int getRubyPosition() {
+ return rubyPosition;
+ }
+
@Nullable
public Layout.Alignment getTextAlign() {
return textAlign;
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 7ec51fc009..54dc5c058d 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
@@ -27,6 +27,7 @@ import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Subtitle;
import com.google.android.exoplayer2.text.SubtitleDecoderException;
+import com.google.android.exoplayer2.text.span.RubySpan;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ColorParser;
import java.io.IOException;
@@ -61,6 +62,7 @@ public final class TtmlDecoderTest {
private static final String BITMAP_UNSUPPORTED_REGION_FILE = "ttml/bitmap_unsupported_region.xml";
private static final String VERTICAL_TEXT_FILE = "ttml/vertical_text.xml";
private static final String TEXT_COMBINE_FILE = "ttml/text_combine.xml";
+ private static final String RUBIES_FILE = "ttml/rubies.xml";
@Test
public void testInlineAttributes() throws IOException, SubtitleDecoderException {
@@ -606,6 +608,38 @@ public final class TtmlDecoderTest {
assertThat(thirdCue).hasNoHorizontalTextInVerticalContextSpanBetween(0, thirdCue.length());
}
+ @Test
+ public void testRubies() throws IOException, SubtitleDecoderException {
+ TtmlSubtitle subtitle = getSubtitle(RUBIES_FILE);
+
+ Spanned firstCue = getOnlyCueTextAtTimeUs(subtitle, 10_000_000);
+ assertThat(firstCue.toString()).isEqualTo("Cue with annotated text.");
+ assertThat(firstCue)
+ .hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length())
+ .withTextAndPosition("1st rubies", RubySpan.POSITION_OVER);
+ assertThat(firstCue)
+ .hasRubySpanBetween("Cue with annotated ".length(), "Cue with annotated text".length())
+ .withTextAndPosition("2nd rubies", RubySpan.POSITION_UNKNOWN);
+
+ Spanned secondCue = getOnlyCueTextAtTimeUs(subtitle, 20_000_000);
+ assertThat(secondCue.toString()).isEqualTo("Cue with annotated text.");
+ assertThat(secondCue)
+ .hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length())
+ .withTextAndPosition("rubies", RubySpan.POSITION_UNKNOWN);
+
+ Spanned thirdCue = getOnlyCueTextAtTimeUs(subtitle, 30_000_000);
+ assertThat(thirdCue.toString()).isEqualTo("Cue with annotated text.");
+ assertThat(thirdCue).hasNoRubySpanBetween(0, thirdCue.length());
+
+ Spanned fourthCue = getOnlyCueTextAtTimeUs(subtitle, 40_000_000);
+ assertThat(fourthCue.toString()).isEqualTo("Cue with text.");
+ assertThat(fourthCue).hasNoRubySpanBetween(0, fourthCue.length());
+
+ Spanned fifthCue = getOnlyCueTextAtTimeUs(subtitle, 50_000_000);
+ assertThat(fifthCue.toString()).isEqualTo("Cue with annotated text.");
+ assertThat(fifthCue).hasNoRubySpanBetween(0, fifthCue.length());
+ }
+
private static Spanned getOnlyCueTextAtTimeUs(Subtitle subtitle, long timeUs) {
Cue cue = getOnlyCueAtTimeUs(subtitle, timeUs);
assertThat(cue.text).isInstanceOf(Spanned.class);
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 d2fb155f8a..8f6cc97a30 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
@@ -29,6 +29,7 @@ import android.text.Layout;
import androidx.annotation.ColorInt;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.span.RubySpan;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -42,6 +43,8 @@ public final class TtmlStyleTest {
private static final float FONT_SIZE = 12.5f;
@TtmlStyle.FontSizeUnit private static final int FONT_SIZE_UNIT = TtmlStyle.FONT_SIZE_UNIT_EM;
@ColorInt private static final int BACKGROUND_COLOR = Color.BLACK;
+ private static final int RUBY_TYPE = TtmlStyle.RUBY_TYPE_TEXT;
+ private static final int RUBY_POSITION = RubySpan.POSITION_UNDER;
private static final Layout.Alignment TEXT_ALIGN = Layout.Alignment.ALIGN_CENTER;
private static final boolean TEXT_COMBINE = true;
@Cue.VerticalType private static final int VERTICAL_TYPE = Cue.VERTICAL_TYPE_RL;
@@ -58,6 +61,8 @@ public final class TtmlStyleTest {
.setFontFamily(FONT_FAMILY)
.setFontSize(FONT_SIZE)
.setFontSizeUnit(FONT_SIZE_UNIT)
+ .setRubyType(RUBY_TYPE)
+ .setRubyPosition(RUBY_POSITION)
.setTextAlign(TEXT_ALIGN)
.setTextCombine(TEXT_COMBINE)
.setVerticalType(VERTICAL_TYPE);
@@ -75,8 +80,12 @@ public final class TtmlStyleTest {
assertThat(style.getFontColor()).isEqualTo(FONT_COLOR);
assertThat(style.getFontSize()).isEqualTo(FONT_SIZE);
assertThat(style.getFontSizeUnit()).isEqualTo(FONT_SIZE_UNIT);
+ assertThat(style.getRubyPosition()).isEqualTo(RUBY_POSITION);
assertThat(style.getTextAlign()).isEqualTo(TEXT_ALIGN);
assertThat(style.getTextCombine()).isEqualTo(TEXT_COMBINE);
+ assertWithMessage("rubyType should not be inherited")
+ .that(style.getRubyType())
+ .isEqualTo(UNSPECIFIED);
assertWithMessage("backgroundColor should not be inherited")
.that(style.hasBackgroundColor())
.isFalse();
@@ -99,11 +108,13 @@ public final class TtmlStyleTest {
assertThat(style.getFontColor()).isEqualTo(FONT_COLOR);
assertThat(style.getFontSize()).isEqualTo(FONT_SIZE);
assertThat(style.getFontSizeUnit()).isEqualTo(FONT_SIZE_UNIT);
+ assertThat(style.getRubyPosition()).isEqualTo(RUBY_POSITION);
assertThat(style.getTextAlign()).isEqualTo(TEXT_ALIGN);
assertThat(style.getTextCombine()).isEqualTo(TEXT_COMBINE);
assertWithMessage("backgroundColor should be chained")
.that(style.getBackgroundColor())
.isEqualTo(BACKGROUND_COLOR);
+ assertWithMessage("rubyType should be chained").that(style.getRubyType()).isEqualTo(RUBY_TYPE);
assertWithMessage("verticalType should be chained")
.that(style.getVerticalType())
.isEqualTo(VERTICAL_TYPE);
@@ -206,6 +217,24 @@ public final class TtmlStyleTest {
assertThat(style.getId()).isNull();
}
+ @Test
+ public void testRubyType() {
+ TtmlStyle style = new TtmlStyle();
+
+ assertThat(style.getRubyType()).isEqualTo(UNSPECIFIED);
+ style.setRubyType(TtmlStyle.RUBY_TYPE_BASE);
+ assertThat(style.getRubyType()).isEqualTo(TtmlStyle.RUBY_TYPE_BASE);
+ }
+
+ @Test
+ public void testRubyPosition() {
+ TtmlStyle style = new TtmlStyle();
+
+ assertThat(style.getRubyPosition()).isEqualTo(RubySpan.POSITION_UNKNOWN);
+ style.setRubyPosition(RubySpan.POSITION_OVER);
+ assertThat(style.getRubyPosition()).isEqualTo(RubySpan.POSITION_OVER);
+ }
+
@Test
public void testTextAlign() {
TtmlStyle style = new TtmlStyle();
diff --git a/testdata/src/test/assets/ttml/rubies.xml b/testdata/src/test/assets/ttml/rubies.xml
new file mode 100644
index 0000000000..0eb89da477
--- /dev/null
+++ b/testdata/src/test/assets/ttml/rubies.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+ Cue with
+
+ annotated
+ 1st rubies
+
+
+ 2nd rubies
+ text.
+
+
+
+
+
+
+ Cue with
+
+ rubies
+ annotated
+ alt-text
+
+ text.
+
+
+
+
+
+ Cue with
+
+ annotated
+
+ text.
+
+
+
+
+ Cue with
+
+ rubies
+
+ text.
+
+
+
+
+
+ Cue with
+ rubies
+ annotated
+ text.
+
+
+
+