mirror of
https://github.com/samsonjs/media.git
synced 2026-04-08 11:45:51 +00:00
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:
parent
2b1a066339
commit
6f312c054e
3 changed files with 97 additions and 7 deletions
|
|
@ -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) ###
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 "
|
||||
|
|
|
|||
Loading…
Reference in a new issue