Add <ruby> tag support to WebvttCueParser

There's currently no rendering support for ruby text in SubtitleView
or SubtitlePainter, but this does have a visible impact with the
current implementation by stripping the ruby text from Cue.text
meaning it doesn't show up at all under the 'naive' rendering.
This is an improvement over the current behaviour of including
the ruby text in-line with the base text (no rubies is better than
wrongly rendered rubies).

PiperOrigin-RevId: 288280416
This commit is contained in:
ibaker 2020-01-06 12:52:59 +00:00 committed by Ian Baker
parent 2b1a066339
commit 6f312c054e
3 changed files with 97 additions and 7 deletions

View file

@ -45,6 +45,8 @@
* Improve support for G.711 A-law and mu-law encoded data.
* Fix MKV subtitles to disappear when intended instead of lasting until the
next cue ([#6833](https://github.com/google/ExoPlayer/issues/6833)).
* Parse \<ruby\> and \<rt\> tags in WebVTT subtitles (rendering is coming
later).
### 2.11.1 (2019-12-20) ###

View file

@ -37,6 +37,7 @@ import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.span.RubySpan;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray;
@ -120,11 +121,13 @@ public final class WebvttCueParser {
private static final String ENTITY_NON_BREAK_SPACE = "nbsp";
private static final String TAG_BOLD = "b";
private static final String TAG_ITALIC = "i";
private static final String TAG_UNDERLINE = "u";
private static final String TAG_CLASS = "c";
private static final String TAG_VOICE = "v";
private static final String TAG_ITALIC = "i";
private static final String TAG_LANG = "lang";
private static final String TAG_RUBY = "ruby";
private static final String TAG_RUBY_TEXT = "rt";
private static final String TAG_UNDERLINE = "u";
private static final String TAG_VOICE = "v";
private static final int STYLE_BOLD = Typeface.BOLD;
private static final int STYLE_ITALIC = Typeface.ITALIC;
@ -197,6 +200,7 @@ public final class WebvttCueParser {
ArrayDeque<StartTag> startTagStack = new ArrayDeque<>();
List<StyleMatch> scratchStyleMatches = new ArrayList<>();
int pos = 0;
List<Element> nestedElements = new ArrayList<>();
while (pos < markup.length()) {
char curr = markup.charAt(pos);
switch (curr) {
@ -225,8 +229,14 @@ public final class WebvttCueParser {
break;
}
startTag = startTagStack.pop();
applySpansForTag(id, startTag, spannedText, styles, scratchStyleMatches);
} while(!startTag.name.equals(tagName));
applySpansForTag(
id, startTag, nestedElements, spannedText, styles, scratchStyleMatches);
if (!startTagStack.isEmpty()) {
nestedElements.add(new Element(startTag, spannedText.length()));
} else {
nestedElements.clear();
}
} while (!startTag.name.equals(tagName));
} else if (!isVoidTag) {
startTagStack.push(StartTag.buildStartTag(fullTagExpression, spannedText.length()));
}
@ -256,9 +266,15 @@ public final class WebvttCueParser {
}
// apply unclosed tags
while (!startTagStack.isEmpty()) {
applySpansForTag(id, startTagStack.pop(), spannedText, styles, scratchStyleMatches);
applySpansForTag(
id, startTagStack.pop(), nestedElements, spannedText, styles, scratchStyleMatches);
}
applySpansForTag(id, StartTag.buildWholeCueVirtualTag(), spannedText, styles,
applySpansForTag(
id,
StartTag.buildWholeCueVirtualTag(),
/* nestedElements= */ Collections.emptyList(),
spannedText,
styles,
scratchStyleMatches);
return SpannedString.valueOf(spannedText);
}
@ -442,6 +458,8 @@ public final class WebvttCueParser {
case TAG_CLASS:
case TAG_ITALIC:
case TAG_LANG:
case TAG_RUBY:
case TAG_RUBY_TEXT:
case TAG_UNDERLINE:
case TAG_VOICE:
return true;
@ -453,6 +471,7 @@ public final class WebvttCueParser {
private static void applySpansForTag(
@Nullable String cueId,
StartTag startTag,
List<Element> nestedElements,
SpannableStringBuilder text,
List<WebvttCssStyle> styles,
List<StyleMatch> scratchStyleMatches) {
@ -467,6 +486,29 @@ public final class WebvttCueParser {
text.setSpan(new StyleSpan(STYLE_ITALIC), start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break;
case TAG_RUBY:
@Nullable Element rubyTextElement = null;
for (int i = 0; i < nestedElements.size(); i++) {
if (TAG_RUBY_TEXT.equals(nestedElements.get(i).startTag.name)) {
rubyTextElement = nestedElements.get(i);
// Behaviour of multiple <rt> tags inside <ruby> is undefined, so use the first one.
break;
}
}
if (rubyTextElement == null) {
break;
}
// Move the rubyText from spannedText into the RubySpan.
CharSequence rubyText =
text.subSequence(rubyTextElement.startTag.position, rubyTextElement.endPosition);
text.delete(rubyTextElement.startTag.position, rubyTextElement.endPosition);
end -= rubyText.length();
text.setSpan(
new RubySpan(rubyText.toString(), RubySpan.POSITION_OVER),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break;
case TAG_UNDERLINE:
text.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break;
@ -787,4 +829,19 @@ public final class WebvttCueParser {
}
}
/** Information about a complete element (i.e. start tag and end position). */
private static class Element {
private final StartTag startTag;
/**
* The position of the end of this element's text in the un-marked-up cue text (i.e. the
* corollary to {@link StartTag#position}).
*/
private final int endPosition;
private Element(StartTag startTag, int endPosition) {
this.startTag = startTag;
this.endPosition = endPosition;
}
}
}

View file

@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat;
import android.text.Spanned;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.text.span.RubySpan;
import java.util.Collections;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -48,6 +49,36 @@ public final class WebvttCueParserTest {
assertThat(text).hasNoSpans();
}
@Test
public void testParseRubyTag() throws Exception {
Spanned text =
parseCueText("Some <ruby>base text<rt>with ruby</rt></ruby> and undecorated text");
// The text between the <rt> tags is stripped from Cue.text and only present on the RubySpan.
assertThat(text.toString()).isEqualTo("Some base text and undecorated text");
assertThat(text)
.hasRubySpanBetween("Some ".length(), "Some base text".length())
.withTextAndPosition("with ruby", RubySpan.POSITION_OVER);
}
@Test
public void testParseRubyTagWithNoTextTag() throws Exception {
Spanned text = parseCueText("Some <ruby>base text with no ruby text</ruby>");
assertThat(text.toString()).isEqualTo("Some base text with no ruby text");
assertThat(text).hasNoSpans();
}
@Test
public void testParseRubyTagWithEmptyTextTag() throws Exception {
Spanned text = parseCueText("Some <ruby>base text with<rt></rt></ruby> empty ruby text");
assertThat(text.toString()).isEqualTo("Some base text with empty ruby text");
assertThat(text)
.hasRubySpanBetween("Some ".length(), "Some base text with".length())
.withTextAndPosition("", RubySpan.POSITION_OVER);
}
@Test
public void testParseWellFormedUnclosedEndAtCueEnd() throws Exception {
Spanned text = parseCueText("An <u some trailing stuff>unclosed u tag with "