diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 30dc304d25..68ce72a815 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -128,6 +128,7 @@ of which are supported. * Ignore excess characters in CEA-608 lines (max length is 32) ([#7341](https://github.com/google/ExoPlayer/issues/7341)). + * Add support for WebVTT's `ruby-position` CSS property. * 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/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index b26598424f..f9220c44f7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -538,8 +538,6 @@ public final class WebvttCueParser { List scratchStyleMatches) { int start = startTag.position; int end = text.length(); - scratchStyleMatches.clear(); - getApplicableStyles(styles, cueId, startTag, scratchStyleMatches); switch(startTag.name) { case TAG_BOLD: @@ -551,7 +549,7 @@ public final class WebvttCueParser { Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); break; case TAG_RUBY: - applyRubySpans(text, start, nestedElements, scratchStyleMatches); + applyRubySpans(text, cueId, startTag, nestedElements, styles); break; case TAG_UNDERLINE: text.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -567,6 +565,8 @@ public final class WebvttCueParser { return; } + scratchStyleMatches.clear(); + getApplicableStyles(styles, cueId, startTag, scratchStyleMatches); for (int i = 0; i < scratchStyleMatches.size(); i++) { applyStyleToText(text, scratchStyleMatches.get(i).style, start, end); } @@ -574,27 +574,29 @@ public final class WebvttCueParser { private static void applyRubySpans( SpannableStringBuilder text, - int startTagPosition, + @Nullable String cueId, + StartTag startTag, List nestedElements, - List styleMatches) { - @RubySpan.Position int rubyPosition = RubySpan.POSITION_OVER; - for (int i = 0; i < styleMatches.size(); i++) { - WebvttCssStyle style = styleMatches.get(i).style; - if (style.getRubyPosition() != RubySpan.POSITION_UNKNOWN) { - rubyPosition = style.getRubyPosition(); - break; - } - } + List styles) { + @RubySpan.Position int rubyTagPosition = getRubyPosition(styles, cueId, startTag); List sortedNestedElements = new ArrayList<>(nestedElements.size()); sortedNestedElements.addAll(nestedElements); Collections.sort(sortedNestedElements, Element.BY_START_POSITION_ASC); int deletedCharCount = 0; - int lastRubyTextEnd = startTagPosition; + int lastRubyTextEnd = startTag.position; for (int i = 0; i < sortedNestedElements.size(); i++) { if (!TAG_RUBY_TEXT.equals(sortedNestedElements.get(i).startTag.name)) { continue; } Element rubyTextElement = sortedNestedElements.get(i); + // Use the element's ruby-position if set, otherwise the element's and otherwise + // default to OVER. + @RubySpan.Position + int rubyPosition = + firstKnownRubyPosition( + getRubyPosition(styles, cueId, rubyTextElement.startTag), + rubyTagPosition, + RubySpan.POSITION_OVER); // Move the rubyText from spannedText into the RubySpan. int adjustedRubyTextStart = rubyTextElement.startTag.position - deletedCharCount; int adjustedRubyTextEnd = rubyTextElement.endPosition - deletedCharCount; @@ -611,6 +613,37 @@ public final class WebvttCueParser { } } + @RubySpan.Position + private static int getRubyPosition( + List styles, @Nullable String cueId, StartTag startTag) { + List styleMatches = new ArrayList<>(); + getApplicableStyles(styles, cueId, startTag, styleMatches); + for (int i = 0; i < styleMatches.size(); i++) { + WebvttCssStyle style = styleMatches.get(i).style; + if (style.getRubyPosition() != RubySpan.POSITION_UNKNOWN) { + return style.getRubyPosition(); + } + } + return RubySpan.POSITION_UNKNOWN; + } + + @RubySpan.Position + private static int firstKnownRubyPosition( + @RubySpan.Position int position1, + @RubySpan.Position int position2, + @RubySpan.Position int position3) { + if (position1 != RubySpan.POSITION_UNKNOWN) { + return position1; + } + if (position2 != RubySpan.POSITION_UNKNOWN) { + return position2; + } + if (position3 != RubySpan.POSITION_UNKNOWN) { + return position3; + } + throw new IllegalArgumentException(); + } + /** * Adds {@link ForegroundColorSpan}s and {@link BackgroundColorSpan}s to {@code text} for entries * in {@code classes} that match WebVTT's tags nested in a single span. + // Check many tags with different positions nested in a single span. Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); assertThat(thirdCue.text.toString()).isEqualTo("base1base2base3."); assertThat((Spanned) thirdCue.text) .hasRubySpanBetween(/* start= */ 0, "base1".length()) - .withTextAndPosition("text1", RubySpan.POSITION_OVER); + .withTextAndPosition("over1", RubySpan.POSITION_OVER); assertThat((Spanned) thirdCue.text) .hasRubySpanBetween("base1".length(), "base1base2".length()) - .withTextAndPosition("text2", RubySpan.POSITION_OVER); + .withTextAndPosition("under2", RubySpan.POSITION_UNDER); assertThat((Spanned) thirdCue.text) .hasRubySpanBetween("base1base2".length(), "base1base2base3".length()) - .withTextAndPosition("text3", RubySpan.POSITION_OVER); + .withTextAndPosition("under3", RubySpan.POSITION_UNDER); // Check a span with no tags. Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); diff --git a/testdata/src/test/assets/webvtt/with_rubies b/testdata/src/test/assets/webvtt/with_rubies index 9b448632fa..9ff34596b7 100644 --- a/testdata/src/test/assets/webvtt/with_rubies +++ b/testdata/src/test/assets/webvtt/with_rubies @@ -19,7 +19,7 @@ Some text with under-rubyunder and over-ruby ( NOTE Many individual rubies in a single tag 00:00:05.000 --> 00:00:06.000 -base1text1base2text2base3text3. +base1over1base2under2base3under3. 00:00:07.000 --> 00:00:08.000 Some text with no ruby text.