mirror of
https://github.com/samsonjs/media.git
synced 2026-04-11 12:15:47 +00:00
Add ruby support to TtmlDecoder
I had to expand the info that gets passed around a bit, so I could join up the container, base & text ruby nodes. Spec: https://www.w3.org/TR/2018/REC-ttml2-20181108/#style-attribute-ruby PiperOrigin-RevId: 295931653
This commit is contained in:
parent
4107375c9d
commit
786a1ee82f
9 changed files with 371 additions and 14 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>This span is explicitly handled in {@code TtmlNode#cleanUpText}.
|
||||
*/
|
||||
/* package */ final class DeleteTextSpan {}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<String, Integer> nodeStartsByRegion;
|
||||
private final HashMap<String, Integer> 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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, TtmlStyle> 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<TtmlNode> 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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
78
testdata/src/test/assets/ttml/rubies.xml
vendored
Normal file
78
testdata/src/test/assets/ttml/rubies.xml
vendored
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<!--
|
||||
~ 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.
|
||||
~
|
||||
-->
|
||||
<tt xmlns:ttm="http://www.w3.org/2006/10/ttaf1#metadata"
|
||||
xmlns:ttp="http://www.w3.org/2006/10/ttaf1#parameter"
|
||||
xmlns:tts="http://www.w3.org/2006/10/ttaf1#style"
|
||||
xmlns="http://www.w3.org/ns/ttml"
|
||||
xmlns="http://www.w3.org/2006/10/ttaf1">
|
||||
<body>
|
||||
<div>
|
||||
<!-- Base before and after text, one with explicit position -->
|
||||
<p begin="10s" end="18s">
|
||||
Cue with
|
||||
<span tts:ruby="container" tts:rubyPosition="before">
|
||||
<span tts:ruby="base">annotated</span>
|
||||
<span tts:ruby="text">1st rubies</span>
|
||||
</span>
|
||||
<span tts:ruby="container">
|
||||
<span tts:ruby="text">2nd rubies</span>
|
||||
<span tts:ruby="base">text</span>.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<!-- Delimiter (parenthetical) text is stripped -->
|
||||
<p begin="20s" end="28s">
|
||||
Cue with
|
||||
<span tts:ruby="container">
|
||||
<span tts:ruby="text">rubies</span>
|
||||
<span tts:ruby="base">annotated</span>
|
||||
<span tts:ruby="delimiter">alt-text</span>
|
||||
</span>
|
||||
text.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<!-- No text section -> no span -->
|
||||
<p begin="30s" end="38s">
|
||||
Cue with
|
||||
<span tts:ruby="container" tts:rubyPosition="before">
|
||||
<span tts:ruby="base">annotated</span>
|
||||
</span>
|
||||
text.</p>
|
||||
</div>
|
||||
<div>
|
||||
<!-- No base section -> text still stripped-->
|
||||
<p begin="40s" end="48s">
|
||||
Cue with
|
||||
<span tts:ruby="container" tts:rubyPosition="before">
|
||||
<span tts:ruby="text">rubies</span>
|
||||
</span>
|
||||
text.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<!-- No container section -> text still stripped-->
|
||||
<p begin="50s" end="58s">
|
||||
Cue with
|
||||
<span tts:ruby="text">rubies</span>
|
||||
<span tts:ruby="base">annotated</span>
|
||||
text.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</tt>
|
||||
Loading…
Reference in a new issue