mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Add full selector support to CSS in WebVTT
This CL allows near-complete support to CSS selectors (I say near because not every CSS rule applies to WebVTT). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=120717498
This commit is contained in:
parent
5cbf75b619
commit
81383f8022
9 changed files with 320 additions and 220 deletions
|
|
@ -0,0 +1,40 @@
|
||||||
|
WEBVTT
|
||||||
|
|
||||||
|
STYLE
|
||||||
|
::cue(\n#id ){text-decoration:underline;}
|
||||||
|
|
||||||
|
STYLE
|
||||||
|
::cue(#id.class1.class2 ){ color: violet;}
|
||||||
|
|
||||||
|
STYLE
|
||||||
|
::cue(lang){font-family:Courier}
|
||||||
|
|
||||||
|
STYLE
|
||||||
|
::cue(.class.another ){font-weight: bold;}
|
||||||
|
|
||||||
|
STYLE
|
||||||
|
::cue(v.class[voice="Strider Trancos"] ){ font-weight:bold; }
|
||||||
|
|
||||||
|
STYLE
|
||||||
|
::cue(v#anId.class1.class2[voice="Robert"] ){ font-style:italic; }
|
||||||
|
|
||||||
|
id
|
||||||
|
00:00.000 --> 00:01.001
|
||||||
|
This should be underlined and <lang.class1.class2> courier and violet.
|
||||||
|
|
||||||
|
íd
|
||||||
|
00:02.000 --> 00:02.001
|
||||||
|
This <lang.class1.class2>should be just courier.
|
||||||
|
|
||||||
|
_id
|
||||||
|
00:02.500 --> 00:02.501
|
||||||
|
This <lang.class.another>should be courier and bold.
|
||||||
|
|
||||||
|
00:04.000 --> 00:04.001
|
||||||
|
This <v Strider Trancos> shouldn't be bold.</v>
|
||||||
|
This <v.class.clazz Strider Trancos> should be bold.
|
||||||
|
|
||||||
|
anId
|
||||||
|
00:05.000 --> 00:05.001
|
||||||
|
This is <v.class1.class3.class2 Pipo> specific </v>
|
||||||
|
<v.class1.class3.class2 Robert> But this is more italic</v>
|
||||||
|
|
@ -19,9 +19,6 @@ import com.google.android.exoplayer.util.ParsableByteArray;
|
||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit test for {@link CssParser}.
|
* Unit test for {@link CssParser}.
|
||||||
*/
|
*/
|
||||||
|
|
@ -82,75 +79,46 @@ public final class CssParserTest extends InstrumentationTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testParseMethodSimpleInput() {
|
public void testParseMethodSimpleInput() {
|
||||||
String styleBlock = " ::cue { color : black; background-color: PapayaWhip }";
|
String styleBlock1 = " ::cue { color : black; background-color: PapayaWhip }";
|
||||||
// Expected style map construction.
|
WebvttCssStyle expectedStyle = new WebvttCssStyle();
|
||||||
Map<String, WebvttCssStyle> expectedResult = new HashMap<>();
|
expectedStyle.setFontColor(0xFF000000);
|
||||||
expectedResult.put("", new WebvttCssStyle());
|
expectedStyle.setBackgroundColor(0xFFFFEFD5);
|
||||||
WebvttCssStyle style = expectedResult.get("");
|
assertParserProduces(expectedStyle, styleBlock1);
|
||||||
style.setFontColor(0xFF000000);
|
|
||||||
style.setBackgroundColor(0xFFFFEFD5);
|
|
||||||
|
|
||||||
assertCssProducesExpectedMap(expectedResult, new String[] { styleBlock });
|
String styleBlock2 = " ::cue { color : black }\n\n::cue { color : invalid }";
|
||||||
}
|
expectedStyle = new WebvttCssStyle();
|
||||||
|
expectedStyle.setFontColor(0xFF000000);
|
||||||
|
assertParserProduces(expectedStyle, styleBlock2);
|
||||||
|
|
||||||
public void testParseSimpleInputSeparately() {
|
String styleBlock3 = " \n::cue {\n background-color\n:#00fFFe}";
|
||||||
String styleBlock1 = " ::cue { color : black }\n\n::cue { color : invalid }";
|
expectedStyle = new WebvttCssStyle();
|
||||||
String styleBlock2 = " \n::cue {\n background-color\n:#00fFFe}";
|
expectedStyle.setBackgroundColor(0xFF00FFFE);
|
||||||
|
assertParserProduces(expectedStyle, styleBlock3);
|
||||||
// Expected style map construction.
|
|
||||||
Map<String, WebvttCssStyle> expectedResult = new HashMap<>();
|
|
||||||
expectedResult.put("", new WebvttCssStyle());
|
|
||||||
WebvttCssStyle style = expectedResult.get("");
|
|
||||||
style.setFontColor(0xFF000000);
|
|
||||||
style.setBackgroundColor(0xFF00FFFE);
|
|
||||||
|
|
||||||
assertCssProducesExpectedMap(expectedResult, new String[] { styleBlock1, styleBlock2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testDifferentSelectors() {
|
|
||||||
String styleBlock1 = " ::cue(\n#id ){text-decoration:underline;}";
|
|
||||||
String styleBlock2 = "::cue(elem ){font-family:Courier}";
|
|
||||||
String styleBlock3 = "::cue(.class ){font-weight: bold;}";
|
|
||||||
|
|
||||||
// Expected style map construction.
|
|
||||||
Map<String, WebvttCssStyle> expectedResult = new HashMap<>();
|
|
||||||
expectedResult.put("#id", new WebvttCssStyle().setUnderline(true));
|
|
||||||
expectedResult.put("elem", new WebvttCssStyle().setFontFamily("courier"));
|
|
||||||
expectedResult.put(".class", new WebvttCssStyle().setBold(true));
|
|
||||||
|
|
||||||
assertCssProducesExpectedMap(expectedResult, new String[] { styleBlock1, styleBlock2,
|
|
||||||
styleBlock3});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testMultiplePropertiesInBlock() {
|
public void testMultiplePropertiesInBlock() {
|
||||||
String styleBlock = "::cue(#id){text-decoration:underline; background-color:green;"
|
String styleBlock = "::cue(#id){text-decoration:underline; background-color:green;"
|
||||||
+ "color:red; font-family:Courier; font-weight:bold}";
|
+ "color:red; font-family:Courier; font-weight:bold}";
|
||||||
|
|
||||||
// Expected style map construction.
|
|
||||||
Map<String, WebvttCssStyle> expectedResult = new HashMap<>();
|
|
||||||
WebvttCssStyle expectedStyle = new WebvttCssStyle();
|
WebvttCssStyle expectedStyle = new WebvttCssStyle();
|
||||||
expectedResult.put("#id", expectedStyle);
|
expectedStyle.setTargetId("id");
|
||||||
expectedStyle.setUnderline(true);
|
expectedStyle.setUnderline(true);
|
||||||
expectedStyle.setBackgroundColor(0xFF008000);
|
expectedStyle.setBackgroundColor(0xFF008000);
|
||||||
expectedStyle.setFontColor(0xFFFF0000);
|
expectedStyle.setFontColor(0xFFFF0000);
|
||||||
expectedStyle.setFontFamily("courier");
|
expectedStyle.setFontFamily("courier");
|
||||||
expectedStyle.setBold(true);
|
expectedStyle.setBold(true);
|
||||||
|
|
||||||
assertCssProducesExpectedMap(expectedResult, new String[] { styleBlock });
|
assertParserProduces(expectedStyle, styleBlock);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testRgbaColorExpression() {
|
public void testRgbaColorExpression() {
|
||||||
String styleBlock = "::cue(#rgb){background-color: rgba(\n10/* Ugly color */,11\t, 12\n,.1);"
|
String styleBlock = "::cue(#rgb){background-color: rgba(\n10/* Ugly color */,11\t, 12\n,.1);"
|
||||||
+ "color:rgb(1,1,\n1)}";
|
+ "color:rgb(1,1,\n1)}";
|
||||||
|
|
||||||
// Expected style map construction.
|
|
||||||
Map<String, WebvttCssStyle> expectedResult = new HashMap<>();
|
|
||||||
WebvttCssStyle expectedStyle = new WebvttCssStyle();
|
WebvttCssStyle expectedStyle = new WebvttCssStyle();
|
||||||
expectedResult.put("#rgb", expectedStyle);
|
expectedStyle.setTargetId("rgb");
|
||||||
expectedStyle.setBackgroundColor(0x190A0B0C);
|
expectedStyle.setBackgroundColor(0x190A0B0C);
|
||||||
expectedStyle.setFontColor(0xFF010101);
|
expectedStyle.setFontColor(0xFF010101);
|
||||||
|
|
||||||
assertCssProducesExpectedMap(expectedResult, new String[] { styleBlock });
|
assertParserProduces(expectedStyle, styleBlock);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testGetNextToken() {
|
public void testGetNextToken() {
|
||||||
|
|
@ -181,20 +149,20 @@ public final class CssParserTest extends InstrumentationTestCase {
|
||||||
public void testStyleScoreSystem() {
|
public void testStyleScoreSystem() {
|
||||||
WebvttCssStyle style = new WebvttCssStyle();
|
WebvttCssStyle style = new WebvttCssStyle();
|
||||||
// Universal selector.
|
// Universal selector.
|
||||||
assertEquals(1, style.getSpecificityScore(null, null, new String[0], null));
|
assertEquals(1, style.getSpecificityScore("", "", new String[0], ""));
|
||||||
// Class match without tag match.
|
// Class match without tag match.
|
||||||
style.setTargetClasses(new String[] { "class1", "class2"});
|
style.setTargetClasses(new String[] { "class1", "class2"});
|
||||||
assertEquals(8, style.getSpecificityScore(null, null,
|
assertEquals(8, style.getSpecificityScore("", "", new String[] { "class1", "class2", "class3" },
|
||||||
new String[] { "class1", "class2", "class3" }, null));
|
""));
|
||||||
// Class and tag match
|
// Class and tag match
|
||||||
style.setTargetTagName("b");
|
style.setTargetTagName("b");
|
||||||
assertEquals(10, style.getSpecificityScore(null, "b",
|
assertEquals(10, style.getSpecificityScore("", "b",
|
||||||
new String[] { "class1", "class2", "class3" }, null));
|
new String[] { "class1", "class2", "class3" }, ""));
|
||||||
// Class insufficiency.
|
// Class insufficiency.
|
||||||
assertEquals(0, style.getSpecificityScore(null, "b", new String[] { "class1", "class" }, null));
|
assertEquals(0, style.getSpecificityScore("", "b", new String[] { "class1", "class" }, ""));
|
||||||
// Voice, classes and tag match.
|
// Voice, classes and tag match.
|
||||||
style.setTargetVoice("Manuel Cráneo");
|
style.setTargetVoice("Manuel Cráneo");
|
||||||
assertEquals(14, style.getSpecificityScore(null, "b",
|
assertEquals(14, style.getSpecificityScore("", "b",
|
||||||
new String[] { "class1", "class2", "class3" }, "Manuel Cráneo"));
|
new String[] { "class1", "class2", "class3" }, "Manuel Cráneo"));
|
||||||
// Voice mismatch.
|
// Voice mismatch.
|
||||||
assertEquals(0, style.getSpecificityScore(null, "b",
|
assertEquals(0, style.getSpecificityScore(null, "b",
|
||||||
|
|
@ -205,7 +173,7 @@ public final class CssParserTest extends InstrumentationTestCase {
|
||||||
new String[] { "class1", "class2", "class3" }, "Manuel Cráneo"));
|
new String[] { "class1", "class2", "class3" }, "Manuel Cráneo"));
|
||||||
// Id mismatch.
|
// Id mismatch.
|
||||||
assertEquals(0, style.getSpecificityScore("id1", "b",
|
assertEquals(0, style.getSpecificityScore("id1", "b",
|
||||||
new String[] { "class1", "class2", "class3" }, null));
|
new String[] { "class1", "class2", "class3" }, ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility methods.
|
// Utility methods.
|
||||||
|
|
@ -222,38 +190,25 @@ public final class CssParserTest extends InstrumentationTestCase {
|
||||||
assertEquals(expectedLine, input.readLine());
|
assertEquals(expectedLine, input.readLine());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertCssProducesExpectedMap(Map<String, WebvttCssStyle> expectedResult,
|
private void assertParserProduces(WebvttCssStyle expected,
|
||||||
String[] styleBlocks){
|
String styleBlock){
|
||||||
Map<String, WebvttCssStyle> actualStyleMap = new HashMap<>();
|
ParsableByteArray input = new ParsableByteArray(styleBlock.getBytes());
|
||||||
for (String s : styleBlocks) {
|
WebvttCssStyle actualElem = parser.parseBlock(input);
|
||||||
ParsableByteArray input = new ParsableByteArray(s.getBytes());
|
assertEquals(expected.hasBackgroundColor(), actualElem.hasBackgroundColor());
|
||||||
parser.parseBlock(input, actualStyleMap);
|
if (expected.hasBackgroundColor()) {
|
||||||
|
assertEquals(expected.getBackgroundColor(), actualElem.getBackgroundColor());
|
||||||
}
|
}
|
||||||
assertStyleMapsAreEqual(expectedResult, actualStyleMap);
|
assertEquals(expected.hasFontColor(), actualElem.hasFontColor());
|
||||||
}
|
if (expected.hasFontColor()) {
|
||||||
|
assertEquals(expected.getFontColor(), actualElem.getFontColor());
|
||||||
private void assertStyleMapsAreEqual(Map<String, WebvttCssStyle> expected,
|
|
||||||
Map<String, WebvttCssStyle> actual) {
|
|
||||||
assertEquals(expected.size(), actual.size());
|
|
||||||
for (String k : expected.keySet()) {
|
|
||||||
WebvttCssStyle expectedElem = expected.get(k);
|
|
||||||
WebvttCssStyle actualElem = actual.get(k);
|
|
||||||
assertEquals(expectedElem.hasBackgroundColor(), actualElem.hasBackgroundColor());
|
|
||||||
if (expectedElem.hasBackgroundColor()) {
|
|
||||||
assertEquals(expectedElem.getBackgroundColor(), actualElem.getBackgroundColor());
|
|
||||||
}
|
|
||||||
assertEquals(expectedElem.hasFontColor(), actualElem.hasFontColor());
|
|
||||||
if (expectedElem.hasFontColor()) {
|
|
||||||
assertEquals(expectedElem.getFontColor(), actualElem.getFontColor());
|
|
||||||
}
|
|
||||||
assertEquals(expectedElem.getFontFamily(), actualElem.getFontFamily());
|
|
||||||
assertEquals(expectedElem.getFontSize(), actualElem.getFontSize());
|
|
||||||
assertEquals(expectedElem.getFontSizeUnit(), actualElem.getFontSizeUnit());
|
|
||||||
assertEquals(expectedElem.getStyle(), actualElem.getStyle());
|
|
||||||
assertEquals(expectedElem.isLinethrough(), actualElem.isLinethrough());
|
|
||||||
assertEquals(expectedElem.isUnderline(), actualElem.isUnderline());
|
|
||||||
assertEquals(expectedElem.getTextAlign(), actualElem.getTextAlign());
|
|
||||||
}
|
}
|
||||||
|
assertEquals(expected.getFontFamily(), actualElem.getFontFamily());
|
||||||
|
assertEquals(expected.getFontSize(), actualElem.getFontSize());
|
||||||
|
assertEquals(expected.getFontSizeUnit(), actualElem.getFontSizeUnit());
|
||||||
|
assertEquals(expected.getStyle(), actualElem.getStyle());
|
||||||
|
assertEquals(expected.isLinethrough(), actualElem.isLinethrough());
|
||||||
|
assertEquals(expected.isUnderline(), actualElem.isUnderline());
|
||||||
|
assertEquals(expected.getTextAlign(), actualElem.getTextAlign());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -223,8 +223,7 @@ public final class WebvttCueParserTest extends InstrumentationTestCase {
|
||||||
|
|
||||||
private static Spanned parseCueText(String string) {
|
private static Spanned parseCueText(String string) {
|
||||||
WebvttCue.Builder builder = new WebvttCue.Builder();
|
WebvttCue.Builder builder = new WebvttCue.Builder();
|
||||||
WebvttCueParser.parseCueText(null, string, builder,
|
WebvttCueParser.parseCueText(null, string, builder, Collections.<WebvttCssStyle>emptyList());
|
||||||
Collections.<String, WebvttCssStyle>emptyMap());
|
|
||||||
return (Spanned) builder.build().text;
|
return (Spanned) builder.build().text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import android.text.Spanned;
|
||||||
import android.text.style.BackgroundColorSpan;
|
import android.text.style.BackgroundColorSpan;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.text.style.StyleSpan;
|
import android.text.style.StyleSpan;
|
||||||
|
import android.text.style.TypefaceSpan;
|
||||||
import android.text.style.UnderlineSpan;
|
import android.text.style.UnderlineSpan;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
@ -43,6 +44,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
||||||
private static final String WITH_BAD_CUE_HEADER_FILE = "webvtt/with_bad_cue_header";
|
private static final String WITH_BAD_CUE_HEADER_FILE = "webvtt/with_bad_cue_header";
|
||||||
private static final String WITH_TAGS_FILE = "webvtt/with_tags";
|
private static final String WITH_TAGS_FILE = "webvtt/with_tags";
|
||||||
private static final String WITH_CSS_STYLES = "webvtt/with_css_styles";
|
private static final String WITH_CSS_STYLES = "webvtt/with_css_styles";
|
||||||
|
private static final String WITH_CSS_COMPLEX_SELECTORS = "webvtt/with_css_complex_selectors";
|
||||||
private static final String EMPTY_FILE = "webvtt/empty";
|
private static final String EMPTY_FILE = "webvtt/empty";
|
||||||
|
|
||||||
public void testParseEmpty() throws IOException {
|
public void testParseEmpty() throws IOException {
|
||||||
|
|
@ -57,9 +59,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testParseTypical() throws IOException {
|
public void testParseTypical() throws IOException {
|
||||||
WebvttParser parser = new WebvttParser();
|
WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_FILE);
|
||||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_FILE);
|
|
||||||
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
|
|
||||||
|
|
||||||
// Test event count.
|
// Test event count.
|
||||||
assertEquals(4, subtitle.getEventTimeCount());
|
assertEquals(4, subtitle.getEventTimeCount());
|
||||||
|
|
@ -70,9 +70,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testParseTypicalWithIds() throws IOException {
|
public void testParseTypicalWithIds() throws IOException {
|
||||||
WebvttParser parser = new WebvttParser();
|
WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_IDS_FILE);
|
||||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_WITH_IDS_FILE);
|
|
||||||
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
|
|
||||||
|
|
||||||
// Test event count.
|
// Test event count.
|
||||||
assertEquals(4, subtitle.getEventTimeCount());
|
assertEquals(4, subtitle.getEventTimeCount());
|
||||||
|
|
@ -83,9 +81,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testParseTypicalWithComments() throws IOException {
|
public void testParseTypicalWithComments() throws IOException {
|
||||||
WebvttParser parser = new WebvttParser();
|
WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_COMMENTS_FILE);
|
||||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_WITH_COMMENTS_FILE);
|
|
||||||
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
|
|
||||||
|
|
||||||
// test event count
|
// test event count
|
||||||
assertEquals(4, subtitle.getEventTimeCount());
|
assertEquals(4, subtitle.getEventTimeCount());
|
||||||
|
|
@ -96,9 +92,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testParseWithTags() throws IOException {
|
public void testParseWithTags() throws IOException {
|
||||||
WebvttParser parser = new WebvttParser();
|
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_TAGS_FILE);
|
||||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_TAGS_FILE);
|
|
||||||
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
|
|
||||||
|
|
||||||
// Test event count.
|
// Test event count.
|
||||||
assertEquals(8, subtitle.getEventTimeCount());
|
assertEquals(8, subtitle.getEventTimeCount());
|
||||||
|
|
@ -111,13 +105,9 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testParseWithPositioning() throws IOException {
|
public void testParseWithPositioning() throws IOException {
|
||||||
WebvttParser parser = new WebvttParser();
|
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_POSITIONING_FILE);
|
||||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_POSITIONING_FILE);
|
|
||||||
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
|
|
||||||
|
|
||||||
// Test event count.
|
// Test event count.
|
||||||
assertEquals(12, subtitle.getEventTimeCount());
|
assertEquals(12, subtitle.getEventTimeCount());
|
||||||
|
|
||||||
// Test cues.
|
// Test cues.
|
||||||
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.", Alignment.ALIGN_NORMAL,
|
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.", Alignment.ALIGN_NORMAL,
|
||||||
Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, 0.1f, Cue.ANCHOR_TYPE_START, 0.35f);
|
Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, 0.1f, Cue.ANCHOR_TYPE_START, 0.35f);
|
||||||
|
|
@ -133,15 +123,13 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
||||||
assertCue(subtitle, 8, 7000000, 8000000, "This is the fifth subtitle.",
|
assertCue(subtitle, 8, 7000000, 8000000, "This is the fifth subtitle.",
|
||||||
Alignment.ALIGN_OPPOSITE, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, 0.1f,
|
Alignment.ALIGN_OPPOSITE, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, 0.1f,
|
||||||
Cue.ANCHOR_TYPE_END, 0.1f);
|
Cue.ANCHOR_TYPE_END, 0.1f);
|
||||||
assertCue(subtitle, 10, 10000000, 11000000, "This is the sixth subtitle.",
|
assertCue(subtitle, 10, 10000000, 11000000, "This is the sixth subtitle.",
|
||||||
Alignment.ALIGN_CENTER, 0.45f, Cue.LINE_TYPE_FRACTION, Cue.ANCHOR_TYPE_END, Cue.DIMEN_UNSET,
|
Alignment.ALIGN_CENTER, 0.45f, Cue.LINE_TYPE_FRACTION, Cue.ANCHOR_TYPE_END, Cue.DIMEN_UNSET,
|
||||||
Cue.TYPE_UNSET, 0.35f);
|
Cue.TYPE_UNSET, 0.35f);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testParseWithBadCueHeader() throws IOException {
|
public void testParseWithBadCueHeader() throws IOException {
|
||||||
WebvttParser parser = new WebvttParser();
|
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BAD_CUE_HEADER_FILE);
|
||||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_BAD_CUE_HEADER_FILE);
|
|
||||||
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
|
|
||||||
|
|
||||||
// Test event count.
|
// Test event count.
|
||||||
assertEquals(4, subtitle.getEventTimeCount());
|
assertEquals(4, subtitle.getEventTimeCount());
|
||||||
|
|
@ -150,11 +138,9 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
||||||
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
|
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
|
||||||
assertCue(subtitle, 2, 4000000, 5000000, "This is the third subtitle.");
|
assertCue(subtitle, 2, 4000000, 5000000, "This is the third subtitle.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testWebvttWithCssStyle() throws IOException {
|
public void testWebvttWithCssStyle() throws IOException {
|
||||||
WebvttParser parser = new WebvttParser();
|
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES);
|
||||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_CSS_STYLES);
|
|
||||||
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
|
|
||||||
|
|
||||||
// Test event count.
|
// Test event count.
|
||||||
assertEquals(8, subtitle.getEventTimeCount());
|
assertEquals(8, subtitle.getEventTimeCount());
|
||||||
|
|
@ -162,15 +148,11 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
||||||
// Test cues.
|
// Test cues.
|
||||||
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
|
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
|
||||||
assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.");
|
assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.");
|
||||||
|
|
||||||
Cue cue1 = subtitle.getCues(0).get(0);
|
Spanned s1 = getUniqueSpanTextAt(subtitle, 0);
|
||||||
Cue cue2 = subtitle.getCues(2345000).get(0);
|
Spanned s2 = getUniqueSpanTextAt(subtitle, 2345000);
|
||||||
Cue cue3 = subtitle.getCues(20000000).get(0);
|
Spanned s3 = getUniqueSpanTextAt(subtitle, 20000000);
|
||||||
Cue cue4 = subtitle.getCues(25000000).get(0);
|
Spanned s4 = getUniqueSpanTextAt(subtitle, 25000000);
|
||||||
Spanned s1 = (Spanned) cue1.text;
|
|
||||||
Spanned s2 = (Spanned) cue2.text;
|
|
||||||
Spanned s3 = (Spanned) cue3.text;
|
|
||||||
Spanned s4 = (Spanned) cue4.text;
|
|
||||||
assertEquals(1, s1.getSpans(0, s1.length(), ForegroundColorSpan.class).length);
|
assertEquals(1, s1.getSpans(0, s1.length(), ForegroundColorSpan.class).length);
|
||||||
assertEquals(1, s1.getSpans(0, s1.length(), BackgroundColorSpan.class).length);
|
assertEquals(1, s1.getSpans(0, s1.length(), BackgroundColorSpan.class).length);
|
||||||
assertEquals(2, s2.getSpans(0, s2.length(), ForegroundColorSpan.class).length);
|
assertEquals(2, s2.getSpans(0, s2.length(), ForegroundColorSpan.class).length);
|
||||||
|
|
@ -180,6 +162,46 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
||||||
assertEquals(Typeface.BOLD, s4.getSpans(17, s4.length(), StyleSpan.class)[0].getStyle());
|
assertEquals(Typeface.BOLD, s4.getSpans(17, s4.length(), StyleSpan.class)[0].getStyle());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testWithComplexCssSelectors() throws IOException {
|
||||||
|
WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_COMPLEX_SELECTORS);
|
||||||
|
Spanned text = getUniqueSpanTextAt(subtitle, 0);
|
||||||
|
assertEquals(1, text.getSpans(30, text.length(), ForegroundColorSpan.class).length);
|
||||||
|
assertEquals(0xFFEE82EE,
|
||||||
|
text.getSpans(30, text.length(), ForegroundColorSpan.class)[0].getForegroundColor());
|
||||||
|
assertEquals(1, text.getSpans(30, text.length(), TypefaceSpan.class).length);
|
||||||
|
assertEquals("courier", text.getSpans(30, text.length(), TypefaceSpan.class)[0].getFamily());
|
||||||
|
|
||||||
|
text = getUniqueSpanTextAt(subtitle, 2000000);
|
||||||
|
assertEquals(1, text.getSpans(5, text.length(), TypefaceSpan.class).length);
|
||||||
|
assertEquals("courier", text.getSpans(5, text.length(), TypefaceSpan.class)[0].getFamily());
|
||||||
|
|
||||||
|
text = getUniqueSpanTextAt(subtitle, 2500000);
|
||||||
|
assertEquals(1, text.getSpans(5, text.length(), StyleSpan.class).length);
|
||||||
|
assertEquals(Typeface.BOLD, text.getSpans(5, text.length(), StyleSpan.class)[0].getStyle());
|
||||||
|
assertEquals(1, text.getSpans(5, text.length(), TypefaceSpan.class).length);
|
||||||
|
assertEquals("courier", text.getSpans(5, text.length(), TypefaceSpan.class)[0].getFamily());
|
||||||
|
|
||||||
|
text = getUniqueSpanTextAt(subtitle, 4000000);
|
||||||
|
assertEquals(0, text.getSpans(6, 22, StyleSpan.class).length);
|
||||||
|
assertEquals(1, text.getSpans(30, text.length(), StyleSpan.class).length);
|
||||||
|
assertEquals(Typeface.BOLD, text.getSpans(30, text.length(), StyleSpan.class)[0].getStyle());
|
||||||
|
|
||||||
|
text = getUniqueSpanTextAt(subtitle, 5000000);
|
||||||
|
assertEquals(0, text.getSpans(9, 17, StyleSpan.class).length);
|
||||||
|
assertEquals(1, text.getSpans(19, text.length(), StyleSpan.class).length);
|
||||||
|
assertEquals(Typeface.ITALIC, text.getSpans(19, text.length(), StyleSpan.class)[0].getStyle());
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebvttSubtitle getSubtitleForTestAsset(String asset) throws IOException {
|
||||||
|
WebvttParser parser = new WebvttParser();
|
||||||
|
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), asset);
|
||||||
|
return parser.decode(bytes, bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) {
|
||||||
|
return (Spanned) sub.getCues(timeUs).get(0).text;
|
||||||
|
}
|
||||||
|
|
||||||
private static void assertCue(WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs,
|
private static void assertCue(WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs,
|
||||||
int endTimeUs, String text) {
|
int endTimeUs, String text) {
|
||||||
assertCue(subtitle, eventTimeIndex, startTimeUs, endTimeUs, text, null, Cue.DIMEN_UNSET,
|
assertCue(subtitle, eventTimeIndex, startTimeUs, endTimeUs, text, null, Cue.DIMEN_UNSET,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ import com.google.android.exoplayer.util.ParsableByteArray;
|
||||||
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Arrays;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
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
|
||||||
|
|
@ -32,9 +34,14 @@ import java.util.Map;
|
||||||
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_TEXT_DECORATION = "text-decoration";
|
private static final String PROPERTY_TEXT_DECORATION = "text-decoration";
|
||||||
|
|
||||||
private static final String VALUE_BOLD = "bold";
|
private static final String VALUE_BOLD = "bold";
|
||||||
private static final String VALUE_UNDERLINE = "underline";
|
private static final String VALUE_UNDERLINE = "underline";
|
||||||
|
private static final String BLOCK_START = "{";
|
||||||
|
private static final String BLOCK_END = "}";
|
||||||
|
private static final String PROPERTY_FONT_STYLE = "font-style";
|
||||||
|
private static final String VALUE_ITALIC = "italic";
|
||||||
|
|
||||||
|
private static final Pattern VOICE_NAME_PATTERN = Pattern.compile("\\[voice=\"([^\"]*)\"\\]");
|
||||||
|
|
||||||
// Temporary utility data structures.
|
// Temporary utility data structures.
|
||||||
private final ParsableByteArray styleInput;
|
private final ParsableByteArray styleInput;
|
||||||
|
|
@ -51,57 +58,41 @@ import java.util.Map;
|
||||||
* {@code null} otherwise.
|
* {@code null} otherwise.
|
||||||
*
|
*
|
||||||
* @param input The input from which the style block should be read.
|
* @param input The input from which the style block should be read.
|
||||||
* @param styleMap The map that contains styles accessible by selector.
|
* @return A {@link WebvttCssStyle} that represents the parsed block.
|
||||||
*/
|
*/
|
||||||
public void parseBlock(ParsableByteArray input, Map<String, WebvttCssStyle> styleMap) {
|
public WebvttCssStyle parseBlock(ParsableByteArray input) {
|
||||||
stringBuilder.setLength(0);
|
stringBuilder.setLength(0);
|
||||||
int initialInputPosition = input.getPosition();
|
int initialInputPosition = input.getPosition();
|
||||||
skipStyleBlock(input);
|
skipStyleBlock(input);
|
||||||
styleInput.reset(input.data, input.getPosition());
|
styleInput.reset(input.data, input.getPosition());
|
||||||
styleInput.setPosition(initialInputPosition);
|
styleInput.setPosition(initialInputPosition);
|
||||||
String selector = parseSelector(styleInput, stringBuilder);
|
String selector = parseSelector(styleInput, stringBuilder);
|
||||||
if (selector == null) {
|
if (selector == null || !BLOCK_START.equals(parseNextToken(styleInput, stringBuilder))) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
String token = parseNextToken(styleInput, stringBuilder);
|
WebvttCssStyle style = new WebvttCssStyle();
|
||||||
if (!"{".equals(token)) {
|
applySelectorToStyle(style, selector);
|
||||||
return;
|
String token = null;
|
||||||
}
|
|
||||||
if (!styleMap.containsKey(selector)) {
|
|
||||||
styleMap.put(selector, new WebvttCssStyle());
|
|
||||||
}
|
|
||||||
WebvttCssStyle style = styleMap.get(selector);
|
|
||||||
boolean blockEndFound = false;
|
boolean blockEndFound = false;
|
||||||
while (!blockEndFound) {
|
while (!blockEndFound) {
|
||||||
int position = styleInput.getPosition();
|
int position = styleInput.getPosition();
|
||||||
token = parseNextToken(styleInput, stringBuilder);
|
token = parseNextToken(styleInput, stringBuilder);
|
||||||
if (token == null || "}".equals(token)) {
|
blockEndFound = token == null || BLOCK_END.equals(token);
|
||||||
blockEndFound = true;
|
if (!blockEndFound) {
|
||||||
} else {
|
|
||||||
styleInput.setPosition(position);
|
styleInput.setPosition(position);
|
||||||
parseStyleDeclaration(styleInput, style, stringBuilder);
|
parseStyleDeclaration(styleInput, style, stringBuilder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Only one style block may appear after a STYLE line.
|
return BLOCK_END.equals(token) ? style : null; // Check that the style block ended correctly.
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a string containing the selector. {@link WebvttCueParser#UNIVERSAL_CUE_ID} is the
|
* Returns a string containing the selector. The input is expected to have the form
|
||||||
* universal selector, and null means syntax error.
|
* {@code ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional.
|
||||||
*
|
|
||||||
* <p>Expected inputs are:
|
|
||||||
* <ul>
|
|
||||||
* <li>::cue
|
|
||||||
* <li>::cue(#id)
|
|
||||||
* <li>::cue(elem)
|
|
||||||
* <li>::cue(.class)
|
|
||||||
* <li>::cue(elem.class)
|
|
||||||
* <li>::cue(v[voice="Someone"])
|
|
||||||
* </ul>
|
|
||||||
*
|
*
|
||||||
* @param input From which the selector is obtained.
|
* @param input From which the selector is obtained.
|
||||||
* @return A string containing the target, {@link WebvttCueParser#UNIVERSAL_CUE_ID} if the
|
* @return A string containing the target, empty string if the selector is universal
|
||||||
* selector is universal (targets all cues) or null if an error was encountered.
|
* (targets all cues) or null if an error was encountered.
|
||||||
*/
|
*/
|
||||||
private static String parseSelector(ParsableByteArray input, StringBuilder stringBuilder) {
|
private static String parseSelector(ParsableByteArray input, StringBuilder stringBuilder) {
|
||||||
skipWhitespaceAndComments(input);
|
skipWhitespaceAndComments(input);
|
||||||
|
|
@ -117,9 +108,9 @@ import java.util.Map;
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if ("{".equals(token)) {
|
if (BLOCK_START.equals(token)) {
|
||||||
input.setPosition(position);
|
input.setPosition(position);
|
||||||
return WebvttCueParser.UNIVERSAL_CUE_ID;
|
return "";
|
||||||
}
|
}
|
||||||
String target = null;
|
String target = null;
|
||||||
if ("(".equals(token)) {
|
if ("(".equals(token)) {
|
||||||
|
|
@ -166,7 +157,7 @@ import java.util.Map;
|
||||||
String token = parseNextToken(input, stringBuilder);
|
String token = parseNextToken(input, stringBuilder);
|
||||||
if (";".equals(token)) {
|
if (";".equals(token)) {
|
||||||
// The style declaration is well formed.
|
// The style declaration is well formed.
|
||||||
} else if ("}".equals(token)) {
|
} else if (BLOCK_END.equals(token)) {
|
||||||
// The style declaration is well formed and we can go on, but the closing bracket had to be
|
// The style declaration is well formed and we can go on, but the closing bracket had to be
|
||||||
// fed back.
|
// fed back.
|
||||||
input.setPosition(position);
|
input.setPosition(position);
|
||||||
|
|
@ -189,6 +180,10 @@ import java.util.Map;
|
||||||
if (VALUE_BOLD.equals(value)) {
|
if (VALUE_BOLD.equals(value)) {
|
||||||
style.setBold(true);
|
style.setBold(true);
|
||||||
}
|
}
|
||||||
|
} else if (PROPERTY_FONT_STYLE.equals(property)) {
|
||||||
|
if (VALUE_ITALIC.equals(value)) {
|
||||||
|
style.setItalic(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// TODO: Fill remaining supported styles.
|
// TODO: Fill remaining supported styles.
|
||||||
}
|
}
|
||||||
|
|
@ -256,7 +251,7 @@ import java.util.Map;
|
||||||
// Syntax error.
|
// Syntax error.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if ("}".equals(token) || ";".equals(token)) {
|
if (BLOCK_END.equals(token) || ";".equals(token)) {
|
||||||
input.setPosition(position);
|
input.setPosition(position);
|
||||||
expressionEndFound = true;
|
expressionEndFound = true;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -305,5 +300,34 @@ import java.util.Map;
|
||||||
return stringBuilder.toString();
|
return stringBuilder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
private void applySelectorToStyle(WebvttCssStyle style, String selector) {
|
||||||
|
if ("".equals(selector)) {
|
||||||
|
return; // Universal selector.
|
||||||
|
}
|
||||||
|
int voiceStartPosition = selector.indexOf('[');
|
||||||
|
if (voiceStartPosition != -1) {
|
||||||
|
Matcher matcher = VOICE_NAME_PATTERN.matcher(selector.substring(voiceStartPosition));
|
||||||
|
if (matcher.matches()) {
|
||||||
|
style.setTargetVoice(matcher.group(1));
|
||||||
|
}
|
||||||
|
selector = selector.substring(0, voiceStartPosition);
|
||||||
|
}
|
||||||
|
String[] classDivision = selector.split("\\.");
|
||||||
|
String tagAndIdDivision = classDivision[0];
|
||||||
|
int idPrefixPosition = tagAndIdDivision.indexOf('#');
|
||||||
|
if (idPrefixPosition != -1) {
|
||||||
|
style.setTargetTagName(tagAndIdDivision.substring(0, idPrefixPosition));
|
||||||
|
style.setTargetId(tagAndIdDivision.substring(idPrefixPosition + 1)); // We discard the '#'.
|
||||||
|
} else {
|
||||||
|
style.setTargetTagName(tagAndIdDivision);
|
||||||
|
}
|
||||||
|
if (classDivision.length > 1) {
|
||||||
|
style.setTargetClasses(Arrays.copyOfRange(classDivision, 1, classDivision.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,8 +83,8 @@ public final class Mp4WebvttParser extends SubtitleParser {
|
||||||
if (boxType == TYPE_sttg) {
|
if (boxType == TYPE_sttg) {
|
||||||
WebvttCueParser.parseCueSettingsList(boxPayload, builder);
|
WebvttCueParser.parseCueSettingsList(boxPayload, builder);
|
||||||
} else if (boxType == TYPE_payl) {
|
} else if (boxType == TYPE_payl) {
|
||||||
WebvttCueParser.parseCueText(null, boxPayload.trim(), builder,
|
WebvttCueParser.parseCueText(null, boxPayload.trim(), builder,
|
||||||
Collections.<String, WebvttCssStyle>emptyMap());
|
Collections.<WebvttCssStyle>emptyList());
|
||||||
} else {
|
} else {
|
||||||
// Other VTTCueBox children are still not supported and are ignored.
|
// Other VTTCueBox children are still not supported and are ignored.
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,9 @@ import java.util.List;
|
||||||
public int getSpecificityScore(String id, String tag, String[] classes, String voice) {
|
public int getSpecificityScore(String id, String tag, String[] classes, String voice) {
|
||||||
if (targetId.isEmpty() && targetTag.isEmpty() && targetClasses.isEmpty()
|
if (targetId.isEmpty() && targetTag.isEmpty() && targetClasses.isEmpty()
|
||||||
&& targetVoice.isEmpty()) {
|
&& targetVoice.isEmpty()) {
|
||||||
// The selector is universal. It matches with the minimum score.
|
// The selector is universal. It matches with the minimum score if and only if the given
|
||||||
return 1;
|
// element is a whole cue.
|
||||||
|
return tag.isEmpty() ? 1 : 0;
|
||||||
}
|
}
|
||||||
int score = 0;
|
int score = 0;
|
||||||
score = updateScoreForMatch(score, targetId, id, 0x40000000);
|
score = updateScoreForMatch(score, targetId, id, 0x40000000);
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,10 @@ import android.text.style.TypefaceSpan;
|
||||||
import android.text.style.UnderlineSpan;
|
import android.text.style.UnderlineSpan;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Stack;
|
import java.util.Stack;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
@ -44,7 +47,6 @@ import java.util.regex.Pattern;
|
||||||
*/
|
*/
|
||||||
/* package */ final class WebvttCueParser {
|
/* package */ final class WebvttCueParser {
|
||||||
|
|
||||||
public static final String UNIVERSAL_CUE_ID = "";
|
|
||||||
public static final Pattern CUE_HEADER_PATTERN = Pattern
|
public static final Pattern CUE_HEADER_PATTERN = Pattern
|
||||||
.compile("^(\\S+)\\s+-->\\s+(\\S+)(.*)?$");
|
.compile("^(\\S+)\\s+-->\\s+(\\S+)(.*)?$");
|
||||||
|
|
||||||
|
|
@ -68,10 +70,6 @@ import java.util.regex.Pattern;
|
||||||
private static final String TAG_CLASS = "c";
|
private static final String TAG_CLASS = "c";
|
||||||
private static final String TAG_VOICE = "v";
|
private static final String TAG_VOICE = "v";
|
||||||
private static final String TAG_LANG = "lang";
|
private static final String TAG_LANG = "lang";
|
||||||
|
|
||||||
private static final String CUE_ID_PREFIX = "#";
|
|
||||||
private static final String CUE_VOICE_PREFIX = "v[voice=\"";
|
|
||||||
private static final String CUE_VOICE_SUFFIX = "\"]";
|
|
||||||
|
|
||||||
private static final int STYLE_BOLD = Typeface.BOLD;
|
private static final int STYLE_BOLD = Typeface.BOLD;
|
||||||
private static final int STYLE_ITALIC = Typeface.ITALIC;
|
private static final int STYLE_ITALIC = Typeface.ITALIC;
|
||||||
|
|
@ -83,30 +81,30 @@ import java.util.regex.Pattern;
|
||||||
public WebvttCueParser() {
|
public WebvttCueParser() {
|
||||||
textBuilder = new StringBuilder();
|
textBuilder = new StringBuilder();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the next valid WebVTT cue in a parsable array, including timestamps, settings and text.
|
* Parses the next valid WebVTT cue in a parsable array, including timestamps, settings and text.
|
||||||
*
|
*
|
||||||
* @param webvttData Parsable WebVTT file data.
|
* @param webvttData Parsable WebVTT file data.
|
||||||
* @param builder Builder for WebVTT Cues.
|
* @param builder Builder for WebVTT Cues.
|
||||||
* @param styleMap Maps selector to style as referenced by the CSS ::cue pseudo-element.
|
* @param styles List of styles defined by the CSS style blocks preceeding the cues.
|
||||||
* @return True if a valid Cue was found, false otherwise.
|
* @return True if a valid Cue was found, false otherwise.
|
||||||
*/
|
*/
|
||||||
/* package */ boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder,
|
/* package */ boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder,
|
||||||
Map<String, WebvttCssStyle> styleMap) {
|
List<WebvttCssStyle> styles) {
|
||||||
String firstLine = webvttData.readLine();
|
String firstLine = webvttData.readLine();
|
||||||
Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(firstLine);
|
Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(firstLine);
|
||||||
if (cueHeaderMatcher.matches()) {
|
if (cueHeaderMatcher.matches()) {
|
||||||
// We have found the timestamps in the first line. No id present.
|
// We have found the timestamps in the first line. No id present.
|
||||||
return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styleMap);
|
return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styles);
|
||||||
} else {
|
} else {
|
||||||
// The first line is not the timestamps, but could be the cue id.
|
// The first line is not the timestamps, but could be the cue id.
|
||||||
String secondLine = webvttData.readLine();
|
String secondLine = webvttData.readLine();
|
||||||
cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine);
|
cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine);
|
||||||
if (cueHeaderMatcher.matches()) {
|
if (cueHeaderMatcher.matches()) {
|
||||||
// We can do the rest of the parsing, including the id.
|
// We can do the rest of the parsing, including the id.
|
||||||
return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder,
|
return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder,
|
||||||
styleMap);
|
styles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -148,13 +146,14 @@ import java.util.regex.Pattern;
|
||||||
*
|
*
|
||||||
* @param id Id of the cue, {@code null} if it is not present.
|
* @param id Id of the cue, {@code null} if it is not present.
|
||||||
* @param markup The markup text to be parsed.
|
* @param markup The markup text to be parsed.
|
||||||
* @param styleMap Maps selector to style as referenced by the CSS ::cue pseudo-element.
|
* @param styles List of styles defined by the CSS style blocks preceeding the cues.
|
||||||
* @param builder Target builder.
|
* @param builder Target builder.
|
||||||
*/
|
*/
|
||||||
/* package */ static void parseCueText(String id, String markup, WebvttCue.Builder builder,
|
/* package */ static void parseCueText(String id, String markup, WebvttCue.Builder builder,
|
||||||
Map<String, WebvttCssStyle> styleMap) {
|
List<WebvttCssStyle> styles) {
|
||||||
SpannableStringBuilder spannedText = new SpannableStringBuilder();
|
SpannableStringBuilder spannedText = new SpannableStringBuilder();
|
||||||
Stack<StartTag> startTagStack = new Stack<>();
|
Stack<StartTag> startTagStack = new Stack<>();
|
||||||
|
List<StyleMatch> scratchStyleMatches = new ArrayList<>();
|
||||||
int pos = 0;
|
int pos = 0;
|
||||||
while (pos < markup.length()) {
|
while (pos < markup.length()) {
|
||||||
char curr = markup.charAt(pos);
|
char curr = markup.charAt(pos);
|
||||||
|
|
@ -181,11 +180,10 @@ import java.util.regex.Pattern;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
startTag = startTagStack.pop();
|
startTag = startTagStack.pop();
|
||||||
applySpansForTag(startTag, spannedText, styleMap);
|
applySpansForTag(id, startTag, spannedText, styles, scratchStyleMatches);
|
||||||
} while(!startTag.name.equals(tagName));
|
} while(!startTag.name.equals(tagName));
|
||||||
} else if (!isVoidTag) {
|
} else if (!isVoidTag) {
|
||||||
startTagStack.push(new StartTag(tagName, spannedText.length(),
|
startTagStack.push(StartTag.buildStartTag(fullTagExpression, spannedText.length()));
|
||||||
TAG_VOICE.equals(tagName) ? getVoiceName(fullTagExpression) : null));
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case CHAR_AMPERSAND:
|
case CHAR_AMPERSAND:
|
||||||
|
|
@ -211,16 +209,16 @@ import java.util.regex.Pattern;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// apply unclosed tags
|
// apply unclosed tags
|
||||||
applyStyleToText(spannedText, styleMap.get(UNIVERSAL_CUE_ID), 0, spannedText.length());
|
|
||||||
while (!startTagStack.isEmpty()) {
|
while (!startTagStack.isEmpty()) {
|
||||||
applySpansForTag(startTagStack.pop(), spannedText, styleMap);
|
applySpansForTag(id, startTagStack.pop(), spannedText, styles, scratchStyleMatches);
|
||||||
}
|
}
|
||||||
applyStyleToText(spannedText, styleMap.get(CUE_ID_PREFIX + id), 0, spannedText.length());
|
applySpansForTag(id, StartTag.buildWholeCueVirtualTag(), spannedText, styles,
|
||||||
|
scratchStyleMatches);
|
||||||
builder.setText(spannedText);
|
builder.setText(spannedText);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean parseCue(String id, Matcher cueHeaderMatcher, ParsableByteArray webvttData,
|
private static boolean parseCue(String id, Matcher cueHeaderMatcher, ParsableByteArray webvttData,
|
||||||
WebvttCue.Builder builder, StringBuilder textBuilder, Map<String, WebvttCssStyle> styleMap) {
|
WebvttCue.Builder builder, StringBuilder textBuilder, List<WebvttCssStyle> styles) {
|
||||||
try {
|
try {
|
||||||
// Parse the cue start and end times.
|
// Parse the cue start and end times.
|
||||||
builder.setStartTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)))
|
builder.setStartTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)))
|
||||||
|
|
@ -241,7 +239,7 @@ import java.util.regex.Pattern;
|
||||||
}
|
}
|
||||||
textBuilder.append(line.trim());
|
textBuilder.append(line.trim());
|
||||||
}
|
}
|
||||||
parseCueText(id, textBuilder.toString(), builder, styleMap);
|
parseCueText(id, textBuilder.toString(), builder, styles);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -353,40 +351,40 @@ import java.util.regex.Pattern;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void applySpansForTag(StartTag startTag, SpannableStringBuilder spannedText,
|
private static void applySpansForTag(String cueId, StartTag startTag, SpannableStringBuilder text,
|
||||||
Map<String, WebvttCssStyle> styleMap) {
|
List<WebvttCssStyle> styles, List<StyleMatch> scratchStyleMatches) {
|
||||||
WebvttCssStyle styleForTag = styleMap.get(startTag.name);
|
|
||||||
int start = startTag.position;
|
int start = startTag.position;
|
||||||
int end = spannedText.length();
|
int end = text.length();
|
||||||
switch(startTag.name) {
|
switch(startTag.name) {
|
||||||
case TAG_BOLD:
|
case TAG_BOLD:
|
||||||
spannedText.setSpan(new StyleSpan(STYLE_BOLD), start, end,
|
text.setSpan(new StyleSpan(STYLE_BOLD), start, end,
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
break;
|
break;
|
||||||
case TAG_ITALIC:
|
case TAG_ITALIC:
|
||||||
spannedText.setSpan(new StyleSpan(STYLE_ITALIC), start, end,
|
text.setSpan(new StyleSpan(STYLE_ITALIC), start, end,
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
break;
|
break;
|
||||||
case TAG_UNDERLINE:
|
case TAG_UNDERLINE:
|
||||||
spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
text.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
break;
|
break;
|
||||||
case TAG_CLASS:
|
case TAG_CLASS:
|
||||||
case TAG_LANG:
|
case TAG_LANG:
|
||||||
case TAG_VOICE:
|
case TAG_VOICE:
|
||||||
break;
|
case "": // Case of the "whole cue" virtual tag.
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
applyStyleToText(spannedText, styleForTag, start, end);
|
scratchStyleMatches.clear();
|
||||||
if (startTag.voiceName != null) {
|
getApplicableStyles(styles, cueId, startTag, scratchStyleMatches);
|
||||||
WebvttCssStyle styleForVoice = styleMap.get(CUE_VOICE_PREFIX + startTag.voiceName
|
int styleMatchesCount = scratchStyleMatches.size();
|
||||||
+ CUE_VOICE_SUFFIX);
|
for (int i = 0; i < styleMatchesCount; i++) {
|
||||||
applyStyleToText(spannedText, styleForVoice, start, end);
|
applyStyleToText(text, scratchStyleMatches.get(i).style, start, end);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void applyStyleToText(SpannableStringBuilder spannedText,
|
private static void applyStyleToText(SpannableStringBuilder spannedText, WebvttCssStyle style,
|
||||||
WebvttCssStyle style, int start, int end) {
|
int start, int end) {
|
||||||
if (style == null) {
|
if (style == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -447,21 +445,79 @@ import java.util.regex.Pattern;
|
||||||
}
|
}
|
||||||
return tagExpression.split("[ \\.]")[0];
|
return tagExpression.split("[ \\.]")[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getVoiceName(String fullTagExpression) {
|
private static void getApplicableStyles(List<WebvttCssStyle> declaredStyles, String id,
|
||||||
return fullTagExpression.trim().substring(fullTagExpression.indexOf(" ")).trim();
|
StartTag tag, List<StyleMatch> output) {
|
||||||
|
int styleCount = declaredStyles.size();
|
||||||
|
for (int i = 0; i < styleCount; i++) {
|
||||||
|
WebvttCssStyle style = declaredStyles.get(i);
|
||||||
|
int score = style.getSpecificityScore(id, tag.name, tag.classes, tag.voice);
|
||||||
|
if (score > 0) {
|
||||||
|
output.add(new StyleMatch(score, style));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Collections.sort(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class StyleMatch implements Comparable<StyleMatch> {
|
||||||
|
|
||||||
|
public final int score;
|
||||||
|
public final WebvttCssStyle style;
|
||||||
|
|
||||||
|
public StyleMatch(int score, WebvttCssStyle style) {
|
||||||
|
this.score = score;
|
||||||
|
this.style = style;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(StyleMatch another) {
|
||||||
|
return this.score - another.score;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class StartTag {
|
private static final class StartTag {
|
||||||
|
|
||||||
|
private static final String[] NO_CLASSES = new String[0];
|
||||||
|
|
||||||
public final String name;
|
public final String name;
|
||||||
public final int position;
|
public final int position;
|
||||||
public final String voiceName;
|
public final String voice;
|
||||||
|
public final String[] classes;
|
||||||
|
|
||||||
public StartTag(String name, int position, String voiceName) {
|
private StartTag(String name, int position, String voice, String[] classes) {
|
||||||
this.position = position;
|
this.position = position;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.voiceName = voiceName;
|
this.voice = voice;
|
||||||
|
this.classes = classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StartTag buildStartTag(String fullTagExpression, int position) {
|
||||||
|
fullTagExpression = fullTagExpression.trim();
|
||||||
|
if (fullTagExpression.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int voiceStart = fullTagExpression.indexOf(" ");
|
||||||
|
String voice;
|
||||||
|
if (voiceStart == -1) {
|
||||||
|
voice = "";
|
||||||
|
} else {
|
||||||
|
voice = fullTagExpression.substring(voiceStart).trim();
|
||||||
|
fullTagExpression = fullTagExpression.substring(0, voiceStart);
|
||||||
|
}
|
||||||
|
String[] nameAndClasses = fullTagExpression.split("\\.");
|
||||||
|
String name = nameAndClasses[0];
|
||||||
|
String[] classes;
|
||||||
|
if (nameAndClasses.length > 1) {
|
||||||
|
classes = Arrays.copyOfRange(nameAndClasses, 1, nameAndClasses.length);
|
||||||
|
} else {
|
||||||
|
classes = NO_CLASSES;
|
||||||
|
}
|
||||||
|
return new StartTag(name, position, voice, classes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StartTag buildWholeCueVirtualTag() {
|
||||||
|
return new StartTag("", 0, "", new String[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple WebVTT parser.
|
* A simple WebVTT parser.
|
||||||
|
|
@ -38,32 +38,32 @@ public final class WebvttParser extends SubtitleParser {
|
||||||
private static final int CUE_FOUND = 3;
|
private static final int CUE_FOUND = 3;
|
||||||
private static final String COMMENT_START = "NOTE";
|
private static final String COMMENT_START = "NOTE";
|
||||||
private static final String STYLE_START = "STYLE";
|
private static final String STYLE_START = "STYLE";
|
||||||
|
|
||||||
private final WebvttCueParser cueParser;
|
private final WebvttCueParser cueParser;
|
||||||
private final ParsableByteArray parsableWebvttData;
|
private final ParsableByteArray parsableWebvttData;
|
||||||
private final WebvttCue.Builder webvttCueBuilder;
|
private final WebvttCue.Builder webvttCueBuilder;
|
||||||
private final CssParser cssParser;
|
private final CssParser cssParser;
|
||||||
private final HashMap<String, WebvttCssStyle> styleMap;
|
private final List<WebvttCssStyle> definedStyles;
|
||||||
|
|
||||||
public WebvttParser() {
|
public WebvttParser() {
|
||||||
cueParser = new WebvttCueParser();
|
cueParser = new WebvttCueParser();
|
||||||
parsableWebvttData = new ParsableByteArray();
|
parsableWebvttData = new ParsableByteArray();
|
||||||
webvttCueBuilder = new WebvttCue.Builder();
|
webvttCueBuilder = new WebvttCue.Builder();
|
||||||
cssParser = new CssParser();
|
cssParser = new CssParser();
|
||||||
styleMap = new HashMap<>();
|
definedStyles = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected WebvttSubtitle decode(byte[] bytes, int length) throws ParserException {
|
protected WebvttSubtitle decode(byte[] bytes, int length) throws ParserException {
|
||||||
parsableWebvttData.reset(bytes, length);
|
parsableWebvttData.reset(bytes, length);
|
||||||
// Initialization for consistent starting state.
|
// Initialization for consistent starting state.
|
||||||
webvttCueBuilder.reset();
|
webvttCueBuilder.reset();
|
||||||
styleMap.clear();
|
definedStyles.clear();
|
||||||
|
|
||||||
// Validate the first line of the header, and skip the remainder.
|
// Validate the first line of the header, and skip the remainder.
|
||||||
WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData);
|
WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData);
|
||||||
while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}
|
while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}
|
||||||
|
|
||||||
int eventFound;
|
int eventFound;
|
||||||
ArrayList<WebvttCue> subtitles = new ArrayList<>();
|
ArrayList<WebvttCue> subtitles = new ArrayList<>();
|
||||||
while ((eventFound = getNextEvent(parsableWebvttData)) != END_OF_FILE_FOUND) {
|
while ((eventFound = getNextEvent(parsableWebvttData)) != END_OF_FILE_FOUND) {
|
||||||
|
|
@ -74,9 +74,12 @@ public final class WebvttParser extends SubtitleParser {
|
||||||
throw new ParserException("A style block was found after the first cue.");
|
throw new ParserException("A style block was found after the first cue.");
|
||||||
}
|
}
|
||||||
parsableWebvttData.readLine(); // Consume the "STYLE" header.
|
parsableWebvttData.readLine(); // Consume the "STYLE" header.
|
||||||
cssParser.parseBlock(parsableWebvttData, styleMap);
|
WebvttCssStyle styleBlock = cssParser.parseBlock(parsableWebvttData);
|
||||||
|
if (styleBlock != null) {
|
||||||
|
definedStyles.add(styleBlock);
|
||||||
|
}
|
||||||
} else if (eventFound == CUE_FOUND) {
|
} else if (eventFound == CUE_FOUND) {
|
||||||
if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, styleMap)) {
|
if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, definedStyles)) {
|
||||||
subtitles.add(webvttCueBuilder.build());
|
subtitles.add(webvttCueBuilder.build());
|
||||||
webvttCueBuilder.reset();
|
webvttCueBuilder.reset();
|
||||||
}
|
}
|
||||||
|
|
@ -84,11 +87,11 @@ public final class WebvttParser extends SubtitleParser {
|
||||||
}
|
}
|
||||||
return new WebvttSubtitle(subtitles);
|
return new WebvttSubtitle(subtitles);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Positions the input right before the next event, and returns the kind of event found. Does not
|
* Positions the input right before the next event, and returns the kind of event found. Does not
|
||||||
* consume any data from such event, if any.
|
* consume any data from such event, if any.
|
||||||
*
|
*
|
||||||
* @return The kind of event found.
|
* @return The kind of event found.
|
||||||
*/
|
*/
|
||||||
private static int getNextEvent(ParsableByteArray parsableWebvttData) {
|
private static int getNextEvent(ParsableByteArray parsableWebvttData) {
|
||||||
|
|
@ -110,7 +113,7 @@ public final class WebvttParser extends SubtitleParser {
|
||||||
parsableWebvttData.setPosition(currentInputPosition);
|
parsableWebvttData.setPosition(currentInputPosition);
|
||||||
return foundEvent;
|
return foundEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void skipComment(ParsableByteArray parsableWebvttData) {
|
private static void skipComment(ParsableByteArray parsableWebvttData) {
|
||||||
while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}
|
while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue