Add font-size support to WebVTT CssParser.

This CL addresses the github issue [#8946](https://github.com/google/ExoPlayer/issues/8964). That issue requests support for `font-size` CSS property in WebVTT subtitle format. This CL:
* Adds support for `font-size` property by extending capabilities of WebVTT `CssParser`. Implementation of `font-size` property value parsing is based on the one in `TtmlDecoder`.
* Adds unit test along with test file containing WebVTT subtitles with all currently supported `font-size` units.

#minor-release

PiperOrigin-RevId: 388423859
This commit is contained in:
apodob 2021-08-03 12:25:07 +01:00 committed by Andrew Lewis
parent bffa3e0afb
commit 8cddd4d80d
7 changed files with 149 additions and 13 deletions

View File

@ -117,6 +117,8 @@
* Text: * Text:
* TTML: Inherit the `rubyPosition` value from a containing `<span * TTML: Inherit the `rubyPosition` value from a containing `<span
ruby="container">` element. ruby="container">` element.
* WebVTT: Add support for CSS `font-size` property
([#8964](https://github.com/google/ExoPlayer/issues/8964)).
* Ad playback: * Ad playback:
* Support changing ad break positions in the player logic * Support changing ad break positions in the player logic
([#5067](https://github.com/google/ExoPlayer/issues/5067). ([#5067](https://github.com/google/ExoPlayer/issues/5067).

View File

@ -20,8 +20,10 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.span.TextAnnotation; import com.google.android.exoplayer2.text.span.TextAnnotation;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.ColorParser;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Ascii;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@ -31,9 +33,9 @@ import java.util.regex.Pattern;
* Provides a CSS parser for STYLE blocks in Webvtt files. Supports only a subset of the CSS * Provides a CSS parser for STYLE blocks in Webvtt files. Supports only a subset of the CSS
* features. * features.
*/ */
/* package */ final class CssParser { /* package */ final class WebvttCssParser {
private static final String TAG = "CssParser"; private static final String TAG = "WebvttCssParser";
private static final String RULE_START = "{"; private static final String RULE_START = "{";
private static final String RULE_END = "}"; private static final String RULE_END = "}";
@ -41,6 +43,7 @@ import java.util.regex.Pattern;
private static final String PROPERTY_BGCOLOR = "background-color"; private static final String PROPERTY_BGCOLOR = "background-color";
private static final String PROPERTY_FONT_FAMILY = "font-family"; private static final String PROPERTY_FONT_FAMILY = "font-family";
private static final String PROPERTY_FONT_WEIGHT = "font-weight"; private static final String PROPERTY_FONT_WEIGHT = "font-weight";
private static final String PROPERTY_FONT_SIZE = "font-size";
private static final String PROPERTY_RUBY_POSITION = "ruby-position"; private static final String PROPERTY_RUBY_POSITION = "ruby-position";
private static final String VALUE_OVER = "over"; private static final String VALUE_OVER = "over";
private static final String VALUE_UNDER = "under"; private static final String VALUE_UNDER = "under";
@ -54,12 +57,14 @@ import java.util.regex.Pattern;
private static final String VALUE_ITALIC = "italic"; private static final String VALUE_ITALIC = "italic";
private static final Pattern VOICE_NAME_PATTERN = Pattern.compile("\\[voice=\"([^\"]*)\"\\]"); private static final Pattern VOICE_NAME_PATTERN = Pattern.compile("\\[voice=\"([^\"]*)\"\\]");
private static final Pattern FONT_SIZE_PATTERN =
Pattern.compile("^((?:[0-9]*\\.)?[0-9]+)(px|em|%)$");
// Temporary utility data structures. // Temporary utility data structures.
private final ParsableByteArray styleInput; private final ParsableByteArray styleInput;
private final StringBuilder stringBuilder; private final StringBuilder stringBuilder;
public CssParser() { public WebvttCssParser() {
styleInput = new ParsableByteArray(); styleInput = new ParsableByteArray();
stringBuilder = new StringBuilder(); stringBuilder = new StringBuilder();
} }
@ -213,6 +218,8 @@ import java.util.regex.Pattern;
if (VALUE_ITALIC.equals(value)) { if (VALUE_ITALIC.equals(value)) {
style.setItalic(true); style.setItalic(true);
} }
} else if (PROPERTY_FONT_SIZE.equals(property)) {
parseFontSize(value, style);
} }
// TODO: Fill remaining supported styles. // TODO: Fill remaining supported styles.
} }
@ -336,6 +343,31 @@ import java.util.regex.Pattern;
return stringBuilder.toString(); return stringBuilder.toString();
} }
private static void parseFontSize(String fontSize, WebvttCssStyle style) {
Matcher matcher = FONT_SIZE_PATTERN.matcher(Ascii.toLowerCase(fontSize));
if (!matcher.matches()) {
Log.w(TAG, "Invalid font-size: '" + fontSize + "'.");
return;
}
String unit = Assertions.checkNotNull(matcher.group(2));
switch (unit) {
case "px":
style.setFontSizeUnit(WebvttCssStyle.FONT_SIZE_UNIT_PIXEL);
break;
case "em":
style.setFontSizeUnit(WebvttCssStyle.FONT_SIZE_UNIT_EM);
break;
case "%":
style.setFontSizeUnit(WebvttCssStyle.FONT_SIZE_UNIT_PERCENT);
break;
default:
// this line should never be reached because when the fontSize matches the FONT_SIZE_PATTERN
// unit must be one of: px, em, %
throw new IllegalStateException();
}
style.setFontSize(Float.parseFloat(Assertions.checkNotNull(matcher.group(1))));
}
/** /**
* Sets the target of a {@link WebvttCssStyle} by splitting a selector of the form {@code * Sets the target of a {@link WebvttCssStyle} by splitting a selector of the form {@code
* ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional. * ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional.

View File

@ -263,7 +263,7 @@ public final class WebvttCssStyle {
return this; return this;
} }
public WebvttCssStyle setFontSizeUnit(short unit) { public WebvttCssStyle setFontSizeUnit(@FontSizeUnit int unit) {
this.fontSizeUnit = unit; this.fontSizeUnit = unit;
return this; return this;
} }

View File

@ -44,12 +44,12 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder {
private static final String STYLE_START = "STYLE"; private static final String STYLE_START = "STYLE";
private final ParsableByteArray parsableWebvttData; private final ParsableByteArray parsableWebvttData;
private final CssParser cssParser; private final WebvttCssParser cssParser;
public WebvttDecoder() { public WebvttDecoder() {
super("WebvttDecoder"); super("WebvttDecoder");
parsableWebvttData = new ParsableByteArray(); parsableWebvttData = new ParsableByteArray();
cssParser = new CssParser(); cssParser = new WebvttCssParser();
} }
@Override @Override

View File

@ -15,7 +15,7 @@
*/ */
package com.google.android.exoplayer2.text.webvtt; package com.google.android.exoplayer2.text.webvtt;
import static com.google.android.exoplayer2.text.webvtt.CssParser.parseNextToken; import static com.google.android.exoplayer2.text.webvtt.WebvttCssParser.parseNextToken;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -29,15 +29,15 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
/** Unit test for {@link CssParser}. */ /** Unit test for {@link WebvttCssParser}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public final class CssParserTest { public final class WebvttCssParserTest {
private CssParser parser; private WebvttCssParser parser;
@Before @Before
public void setUp() { public void setUp() {
parser = new CssParser(); parser = new WebvttCssParser();
} }
@Test @Test
@ -232,13 +232,13 @@ public final class CssParserTest {
private void assertSkipsToEndOfSkip(String expectedLine, String s) { private void assertSkipsToEndOfSkip(String expectedLine, String s) {
ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(s)); ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(s));
CssParser.skipWhitespaceAndComments(input); WebvttCssParser.skipWhitespaceAndComments(input);
assertThat(input.readLine()).isEqualTo(expectedLine); assertThat(input.readLine()).isEqualTo(expectedLine);
} }
private void assertInputLimit(String expectedLine, String s) { private void assertInputLimit(String expectedLine, String s) {
ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(s)); ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(s));
CssParser.skipStyleBlock(input); WebvttCssParser.skipStyleBlock(input);
assertThat(input.readLine()).isEqualTo(expectedLine); assertThat(input.readLine()).isEqualTo(expectedLine);
} }

View File

@ -54,6 +54,7 @@ public class WebvttDecoderTest {
private static final String WITH_BAD_CUE_HEADER_FILE = "media/webvtt/with_bad_cue_header"; private static final String WITH_BAD_CUE_HEADER_FILE = "media/webvtt/with_bad_cue_header";
private static final String WITH_TAGS_FILE = "media/webvtt/with_tags"; private static final String WITH_TAGS_FILE = "media/webvtt/with_tags";
private static final String WITH_CSS_STYLES = "media/webvtt/with_css_styles"; private static final String WITH_CSS_STYLES = "media/webvtt/with_css_styles";
private static final String WITH_FONT_SIZE = "media/webvtt/with_font_size";
private static final String WITH_CSS_COMPLEX_SELECTORS = private static final String WITH_CSS_COMPLEX_SELECTORS =
"media/webvtt/with_css_complex_selectors"; "media/webvtt/with_css_complex_selectors";
private static final String WITH_CSS_TEXT_COMBINE_UPRIGHT = private static final String WITH_CSS_TEXT_COMBINE_UPRIGHT =
@ -400,6 +401,58 @@ public class WebvttDecoderTest {
assertThat(secondCue.text.toString()).isEqualTo("This is the third subtitle."); assertThat(secondCue.text.toString()).isEqualTo("This is the third subtitle.");
} }
@Test
public void decodeWithCssFontSizeStyle() throws Exception {
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_FONT_SIZE);
assertThat(subtitle.getEventTimeCount()).isEqualTo(12);
assertThat(subtitle.getEventTime(0)).isEqualTo(0L);
assertThat(subtitle.getEventTime(1)).isEqualTo(2_000_000L);
Cue firstCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0)));
assertThat(firstCue.text.toString()).isEqualTo("Sentence with font-size set to 4.4em.");
assertThat((Spanned) firstCue.text)
.hasRelativeSizeSpanBetween(0, "Sentence with font-size set to 4.4em.".length())
.withSizeChange(4.4f);
assertThat(subtitle.getEventTime(2)).isEqualTo(2_100_000L);
assertThat(subtitle.getEventTime(3)).isEqualTo(2_400_000L);
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
assertThat(secondCue.text.toString()).isEqualTo("Sentence with bad font-size unit.");
assertThat((Spanned) secondCue.text).hasNoSpans();
assertThat(subtitle.getEventTime(4)).isEqualTo(2_500_000L);
assertThat(subtitle.getEventTime(5)).isEqualTo(4_000_000L);
Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
assertThat(thirdCue.text.toString()).isEqualTo("Absolute font-size expressed in px unit!");
assertThat((Spanned) thirdCue.text)
.hasAbsoluteSizeSpanBetween(0, "Absolute font-size expressed in px unit!".length())
.withAbsoluteSize(2);
assertThat(subtitle.getEventTime(6)).isEqualTo(4_500_000L);
assertThat(subtitle.getEventTime(7)).isEqualTo(6_000_000L);
Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
assertThat(fourthCue.text.toString()).isEqualTo("Relative font-size expressed in % unit!");
assertThat((Spanned) fourthCue.text)
.hasRelativeSizeSpanBetween(0, "Relative font-size expressed in % unit!".length())
.withSizeChange(0.035f);
assertThat(subtitle.getEventTime(8)).isEqualTo(6_100_000L);
assertThat(subtitle.getEventTime(9)).isEqualTo(6_400_000L);
Cue fifthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(8)));
assertThat(fifthCue.text.toString()).isEqualTo("Sentence with bad font-size value.");
assertThat((Spanned) secondCue.text).hasNoSpans();
assertThat(subtitle.getEventTime(10)).isEqualTo(6_500_000L);
assertThat(subtitle.getEventTime(11)).isEqualTo(8_000_000L);
Cue sixthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(10)));
assertThat(sixthCue.text.toString())
.isEqualTo("Upper and lower case letters in font-size unit.");
assertThat((Spanned) sixthCue.text)
.hasAbsoluteSizeSpanBetween(0, "Upper and lower case letters in font-size unit.".length())
.withAbsoluteSize(2);
}
@Test @Test
public void webvttWithCssStyle() throws Exception { public void webvttWithCssStyle() throws Exception {
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES); WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES);

View File

@ -0,0 +1,49 @@
WEBVTT
STYLE
::cue(.unit-em) {
font-size: 4.4em;
}
STYLE
::cue(.unit-px) {
font-size: 2px;
}
STYLE
::cue(.unit-percent) {
font-size: 3.5%
}
STYLE
::cue(.case-insensitivity) {
font-size: 2Px;
}
STYLE
::cue(.bad-unit) {
font-size: 4.4ef;
}
STYLE
::cue(.bad-value) {
font-size: 3.5.5%
}
00:00.000 --> 00:02.000
<c.unit-em>Sentence with font-size set to 4.4em.</c>
00:02.100 --> 00:02.400
<c.bad-unit>Sentence with bad font-size unit.</c>
00:02.500 --> 00:04.000
<c.unit-px>Absolute font-size expressed in px unit!</c>
00:04.500 --> 00:06.000
<c.unit-percent>Relative font-size expressed in % unit!</c>
00:06.100 --> 00:06.400
<c.bad-value>Sentence with bad font-size value.</c>
00:06.500 --> 00:08.000
<c.case-insensitivity>Upper and lower case letters in font-size unit.</c>