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:
parent
bffa3e0afb
commit
8cddd4d80d
@ -117,6 +117,8 @@
|
||||
* Text:
|
||||
* TTML: Inherit the `rubyPosition` value from a containing `<span
|
||||
ruby="container">` element.
|
||||
* WebVTT: Add support for CSS `font-size` property
|
||||
([#8964](https://github.com/google/ExoPlayer/issues/8964)).
|
||||
* Ad playback:
|
||||
* Support changing ad break positions in the player logic
|
||||
([#5067](https://github.com/google/ExoPlayer/issues/5067).
|
||||
|
@ -20,8 +20,10 @@ import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.text.span.TextAnnotation;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
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.Util;
|
||||
import com.google.common.base.Ascii;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
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
|
||||
* 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_END = "}";
|
||||
@ -41,6 +43,7 @@ import java.util.regex.Pattern;
|
||||
private static final String PROPERTY_BGCOLOR = "background-color";
|
||||
private static final String PROPERTY_FONT_FAMILY = "font-family";
|
||||
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 VALUE_OVER = "over";
|
||||
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 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.
|
||||
private final ParsableByteArray styleInput;
|
||||
private final StringBuilder stringBuilder;
|
||||
|
||||
public CssParser() {
|
||||
public WebvttCssParser() {
|
||||
styleInput = new ParsableByteArray();
|
||||
stringBuilder = new StringBuilder();
|
||||
}
|
||||
@ -213,6 +218,8 @@ import java.util.regex.Pattern;
|
||||
if (VALUE_ITALIC.equals(value)) {
|
||||
style.setItalic(true);
|
||||
}
|
||||
} else if (PROPERTY_FONT_SIZE.equals(property)) {
|
||||
parseFontSize(value, style);
|
||||
}
|
||||
// TODO: Fill remaining supported styles.
|
||||
}
|
||||
@ -336,6 +343,31 @@ import java.util.regex.Pattern;
|
||||
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
|
||||
* ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional.
|
@ -263,7 +263,7 @@ public final class WebvttCssStyle {
|
||||
return this;
|
||||
}
|
||||
|
||||
public WebvttCssStyle setFontSizeUnit(short unit) {
|
||||
public WebvttCssStyle setFontSizeUnit(@FontSizeUnit int unit) {
|
||||
this.fontSizeUnit = unit;
|
||||
return this;
|
||||
}
|
||||
|
@ -44,12 +44,12 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder {
|
||||
private static final String STYLE_START = "STYLE";
|
||||
|
||||
private final ParsableByteArray parsableWebvttData;
|
||||
private final CssParser cssParser;
|
||||
private final WebvttCssParser cssParser;
|
||||
|
||||
public WebvttDecoder() {
|
||||
super("WebvttDecoder");
|
||||
parsableWebvttData = new ParsableByteArray();
|
||||
cssParser = new CssParser();
|
||||
cssParser = new WebvttCssParser();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -15,7 +15,7 @@
|
||||
*/
|
||||
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 androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
@ -29,15 +29,15 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit test for {@link CssParser}. */
|
||||
/** Unit test for {@link WebvttCssParser}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class CssParserTest {
|
||||
public final class WebvttCssParserTest {
|
||||
|
||||
private CssParser parser;
|
||||
private WebvttCssParser parser;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
parser = new CssParser();
|
||||
parser = new WebvttCssParser();
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -232,13 +232,13 @@ public final class CssParserTest {
|
||||
|
||||
private void assertSkipsToEndOfSkip(String expectedLine, String s) {
|
||||
ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(s));
|
||||
CssParser.skipWhitespaceAndComments(input);
|
||||
WebvttCssParser.skipWhitespaceAndComments(input);
|
||||
assertThat(input.readLine()).isEqualTo(expectedLine);
|
||||
}
|
||||
|
||||
private void assertInputLimit(String expectedLine, String s) {
|
||||
ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(s));
|
||||
CssParser.skipStyleBlock(input);
|
||||
WebvttCssParser.skipStyleBlock(input);
|
||||
assertThat(input.readLine()).isEqualTo(expectedLine);
|
||||
}
|
||||
|
@ -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_TAGS_FILE = "media/webvtt/with_tags";
|
||||
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 =
|
||||
"media/webvtt/with_css_complex_selectors";
|
||||
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.");
|
||||
}
|
||||
|
||||
@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
|
||||
public void webvttWithCssStyle() throws Exception {
|
||||
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES);
|
||||
|
49
testdata/src/test/assets/media/webvtt/with_font_size
vendored
Normal file
49
testdata/src/test/assets/media/webvtt/with_font_size
vendored
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user