Add support to CSS in WebVTT files
This CL adds the support of CSS styling in Cues through id and "universal" cue selector. The more sophisticated selectors will be left for later, because they requier a bit more complex logic. Also narrowed a little bit the responsibilities of the WebvttCueParser to move some to the WebvttParser. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=119547731
This commit is contained in:
parent
087cf9546f
commit
efe76def89
23
library/src/androidTest/assets/webvtt/with_css_styles
Normal file
23
library/src/androidTest/assets/webvtt/with_css_styles
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
WEBVTT
|
||||||
|
|
||||||
|
STYLE
|
||||||
|
::cue {
|
||||||
|
background-color: green;
|
||||||
|
color: papayawhip;
|
||||||
|
}
|
||||||
|
/* Style blocks cannot use blank lines nor "dash dash greater than" */
|
||||||
|
|
||||||
|
NOTE comment blocks can be used between style blocks.
|
||||||
|
|
||||||
|
STYLE
|
||||||
|
::cue(2) {
|
||||||
|
color: peachpuff;
|
||||||
|
}
|
||||||
|
|
||||||
|
1
|
||||||
|
00:00.000 --> 00:01.234
|
||||||
|
This is the first subtitle.
|
||||||
|
|
||||||
|
2
|
||||||
|
00:02.345 --> 00:03.456
|
||||||
|
This is the second subtitle.
|
@ -21,6 +21,9 @@ import com.google.android.exoplayer.text.Cue;
|
|||||||
|
|
||||||
import android.test.InstrumentationTestCase;
|
import android.test.InstrumentationTestCase;
|
||||||
import android.text.Layout.Alignment;
|
import android.text.Layout.Alignment;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.style.BackgroundColorSpan;
|
||||||
|
import android.text.style.ForegroundColorSpan;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -36,6 +39,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
|||||||
private static final String WITH_POSITIONING_FILE = "webvtt/with_positioning";
|
private static final String WITH_POSITIONING_FILE = "webvtt/with_positioning";
|
||||||
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 EMPTY_FILE = "webvtt/empty";
|
private static final String EMPTY_FILE = "webvtt/empty";
|
||||||
|
|
||||||
public void testParseEmpty() throws IOException {
|
public void testParseEmpty() throws IOException {
|
||||||
@ -54,10 +58,10 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
|||||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_FILE);
|
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_FILE);
|
||||||
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
|
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
|
||||||
|
|
||||||
// test event count
|
// Test event count.
|
||||||
assertEquals(4, subtitle.getEventTimeCount());
|
assertEquals(4, subtitle.getEventTimeCount());
|
||||||
|
|
||||||
// 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.");
|
||||||
}
|
}
|
||||||
@ -67,10 +71,10 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
|||||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_WITH_IDS_FILE);
|
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_WITH_IDS_FILE);
|
||||||
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
|
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
|
||||||
|
|
||||||
// test event count
|
// Test event count.
|
||||||
assertEquals(4, subtitle.getEventTimeCount());
|
assertEquals(4, subtitle.getEventTimeCount());
|
||||||
|
|
||||||
// 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.");
|
||||||
}
|
}
|
||||||
@ -93,10 +97,10 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
|||||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_TAGS_FILE);
|
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_TAGS_FILE);
|
||||||
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
|
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
|
||||||
|
|
||||||
// test event count
|
// Test event count.
|
||||||
assertEquals(8, subtitle.getEventTimeCount());
|
assertEquals(8, subtitle.getEventTimeCount());
|
||||||
|
|
||||||
// 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.");
|
||||||
assertCue(subtitle, 4, 4000000, 5000000, "This is the third subtitle.");
|
assertCue(subtitle, 4, 4000000, 5000000, "This is the third subtitle.");
|
||||||
@ -108,10 +112,10 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
|||||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_POSITIONING_FILE);
|
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_POSITIONING_FILE);
|
||||||
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
|
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);
|
||||||
assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.",
|
assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.",
|
||||||
@ -136,14 +140,35 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
|||||||
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_BAD_CUE_HEADER_FILE);
|
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_BAD_CUE_HEADER_FILE);
|
||||||
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
|
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
|
||||||
|
|
||||||
// test event count
|
// Test event count.
|
||||||
assertEquals(4, subtitle.getEventTimeCount());
|
assertEquals(4, subtitle.getEventTimeCount());
|
||||||
|
|
||||||
// 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, 4000000, 5000000, "This is the third subtitle.");
|
assertCue(subtitle, 2, 4000000, 5000000, "This is the third subtitle.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testWebvttWithCssStyle() throws IOException {
|
||||||
|
WebvttParser parser = new WebvttParser();
|
||||||
|
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_CSS_STYLES);
|
||||||
|
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
|
||||||
|
|
||||||
|
// Test event count.
|
||||||
|
assertEquals(4, subtitle.getEventTimeCount());
|
||||||
|
|
||||||
|
// Test cues.
|
||||||
|
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
|
||||||
|
assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.");
|
||||||
|
|
||||||
|
Cue cue1 = subtitle.getCues(0).get(0);
|
||||||
|
Cue cue2 = subtitle.getCues(2345000).get(0);
|
||||||
|
Spanned s1 = (Spanned) cue1.text;
|
||||||
|
Spanned s2 = (Spanned) cue2.text;
|
||||||
|
assertEquals(1, s1.getSpans(0, s1.length(), ForegroundColorSpan.class).length);
|
||||||
|
assertEquals(1, s1.getSpans(0, s1.length(), BackgroundColorSpan.class).length);
|
||||||
|
assertEquals(2, s2.getSpans(0, s2.length(), ForegroundColorSpan.class).length);
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
@ -157,7 +182,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
|
|||||||
assertEquals(endTimeUs, subtitle.getEventTime(eventTimeIndex + 1));
|
assertEquals(endTimeUs, subtitle.getEventTime(eventTimeIndex + 1));
|
||||||
List<Cue> cues = subtitle.getCues(subtitle.getEventTime(eventTimeIndex));
|
List<Cue> cues = subtitle.getCues(subtitle.getEventTime(eventTimeIndex));
|
||||||
assertEquals(1, cues.size());
|
assertEquals(1, cues.size());
|
||||||
// Assert cue properties
|
// Assert cue properties.
|
||||||
Cue cue = cues.get(0);
|
Cue cue = cues.get(0);
|
||||||
assertEquals(text, cue.text.toString());
|
assertEquals(text, cue.text.toString());
|
||||||
assertEquals(textAlignment, cue.textAlignment);
|
assertEquals(textAlignment, cue.textAlignment);
|
||||||
|
@ -25,7 +25,6 @@ import com.google.android.exoplayer.extractor.PositionHolder;
|
|||||||
import com.google.android.exoplayer.extractor.SeekMap;
|
import com.google.android.exoplayer.extractor.SeekMap;
|
||||||
import com.google.android.exoplayer.extractor.TrackOutput;
|
import com.google.android.exoplayer.extractor.TrackOutput;
|
||||||
import com.google.android.exoplayer.extractor.ts.PtsTimestampAdjuster;
|
import com.google.android.exoplayer.extractor.ts.PtsTimestampAdjuster;
|
||||||
import com.google.android.exoplayer.text.webvtt.WebvttCueParser;
|
|
||||||
import com.google.android.exoplayer.text.webvtt.WebvttParserUtil;
|
import com.google.android.exoplayer.text.webvtt.WebvttParserUtil;
|
||||||
import com.google.android.exoplayer.util.MimeTypes;
|
import com.google.android.exoplayer.util.MimeTypes;
|
||||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||||
@ -145,7 +144,7 @@ import java.util.regex.Pattern;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the first cue header and parse the start time.
|
// Find the first cue header and parse the start time.
|
||||||
Matcher cueHeaderMatcher = WebvttCueParser.findNextCueHeader(webvttData);
|
Matcher cueHeaderMatcher = WebvttParserUtil.findNextCueHeader(webvttData);
|
||||||
if (cueHeaderMatcher == null) {
|
if (cueHeaderMatcher == null) {
|
||||||
// No cues found. Don't output a sample, but still output a corresponding track.
|
// No cues found. Don't output a sample, but still output a corresponding track.
|
||||||
buildTrackOutput(0);
|
buildTrackOutput(0);
|
||||||
|
@ -54,6 +54,7 @@ import java.util.Map;
|
|||||||
* @param styleMap The map that contains styles accessible by selector.
|
* @param styleMap The map that contains styles accessible by selector.
|
||||||
*/
|
*/
|
||||||
public void parseBlock(ParsableByteArray input, Map<String, WebvttCssStyle> styleMap) {
|
public void parseBlock(ParsableByteArray input, Map<String, WebvttCssStyle> styleMap) {
|
||||||
|
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());
|
||||||
@ -97,8 +98,8 @@ import java.util.Map;
|
|||||||
* ::cue(v[voice="Someone"])
|
* ::cue(v[voice="Someone"])
|
||||||
*
|
*
|
||||||
* @param input From which the selector is obtained.
|
* @param input From which the selector is obtained.
|
||||||
* @return A string containing the target, empty string if targets all cues and null if an error
|
* @return A string containing the target, {@link WebvttCue#UNIVERSAL_CUE_ID} if targets all cues
|
||||||
* was encountered.
|
* and 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);
|
||||||
@ -116,7 +117,7 @@ import java.util.Map;
|
|||||||
}
|
}
|
||||||
if ("{".equals(token)) {
|
if ("{".equals(token)) {
|
||||||
input.setPosition(position);
|
input.setPosition(position);
|
||||||
return "";
|
return WebvttCue.UNIVERSAL_CUE_ID;
|
||||||
}
|
}
|
||||||
String target = null;
|
String target = null;
|
||||||
if ("(".equals(token)) {
|
if ("(".equals(token)) {
|
||||||
|
@ -18,13 +18,31 @@ package com.google.android.exoplayer.text.webvtt;
|
|||||||
import com.google.android.exoplayer.text.Cue;
|
import com.google.android.exoplayer.text.Cue;
|
||||||
|
|
||||||
import android.text.Layout.Alignment;
|
import android.text.Layout.Alignment;
|
||||||
|
import android.text.Spannable;
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.style.AbsoluteSizeSpan;
|
||||||
|
import android.text.style.AlignmentSpan;
|
||||||
|
import android.text.style.BackgroundColorSpan;
|
||||||
|
import android.text.style.ForegroundColorSpan;
|
||||||
|
import android.text.style.RelativeSizeSpan;
|
||||||
|
import android.text.style.StrikethroughSpan;
|
||||||
|
import android.text.style.StyleSpan;
|
||||||
|
import android.text.style.TypefaceSpan;
|
||||||
|
import android.text.style.UnderlineSpan;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A representation of a WebVTT cue.
|
* A representation of a WebVTT cue.
|
||||||
*/
|
*/
|
||||||
/* package */ final class WebvttCue extends Cue {
|
/* package */ final class WebvttCue extends Cue {
|
||||||
|
|
||||||
|
public static final String UNIVERSAL_CUE_ID = "";
|
||||||
|
|
||||||
|
public final String id;
|
||||||
public final long startTime;
|
public final long startTime;
|
||||||
public final long endTime;
|
public final long endTime;
|
||||||
|
|
||||||
@ -33,13 +51,15 @@ import android.util.Log;
|
|||||||
}
|
}
|
||||||
|
|
||||||
public WebvttCue(long startTime, long endTime, CharSequence text) {
|
public WebvttCue(long startTime, long endTime, CharSequence text) {
|
||||||
this(startTime, endTime, text, null, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET,
|
this(null, startTime, endTime, text, null, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET,
|
||||||
Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
|
Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
|
||||||
}
|
}
|
||||||
|
|
||||||
public WebvttCue(long startTime, long endTime, CharSequence text, Alignment textAlignment,
|
public WebvttCue(String id, long startTime, long endTime, CharSequence text,
|
||||||
float line, int lineType, int lineAnchor, float position, int positionAnchor, float width) {
|
Alignment textAlignment, float line, int lineType, int lineAnchor, float position,
|
||||||
|
int positionAnchor, float width) {
|
||||||
super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width);
|
super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width);
|
||||||
|
this.id = id;
|
||||||
this.startTime = startTime;
|
this.startTime = startTime;
|
||||||
this.endTime = endTime;
|
this.endTime = endTime;
|
||||||
}
|
}
|
||||||
@ -62,9 +82,10 @@ import android.util.Log;
|
|||||||
|
|
||||||
private static final String TAG = "WebvttCueBuilder";
|
private static final String TAG = "WebvttCueBuilder";
|
||||||
|
|
||||||
|
private String id;
|
||||||
private long startTime;
|
private long startTime;
|
||||||
private long endTime;
|
private long endTime;
|
||||||
private CharSequence text;
|
private SpannableStringBuilder text;
|
||||||
private Alignment textAlignment;
|
private Alignment textAlignment;
|
||||||
private float line;
|
private float line;
|
||||||
private int lineType;
|
private int lineType;
|
||||||
@ -92,16 +113,28 @@ import android.util.Log;
|
|||||||
width = Cue.DIMEN_UNSET;
|
width = Cue.DIMEN_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construction methods
|
// Construction methods.
|
||||||
|
|
||||||
public WebvttCue build() {
|
public WebvttCue build() {
|
||||||
|
return build(Collections.<String, WebvttCssStyle>emptyMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebvttCue build(Map<String, WebvttCssStyle> styleMap) {
|
||||||
|
// TODO: Add support for inner spans.
|
||||||
|
maybeApplyStyleToText(styleMap.get(UNIVERSAL_CUE_ID), 0, text.length());
|
||||||
|
maybeApplyStyleToText(styleMap.get(id), 0, text.length());
|
||||||
if (position != Cue.DIMEN_UNSET && positionAnchor == Cue.TYPE_UNSET) {
|
if (position != Cue.DIMEN_UNSET && positionAnchor == Cue.TYPE_UNSET) {
|
||||||
derivePositionAnchorFromAlignment();
|
derivePositionAnchorFromAlignment();
|
||||||
}
|
}
|
||||||
return new WebvttCue(startTime, endTime, text, textAlignment, line, lineType, lineAnchor,
|
return new WebvttCue(id, startTime, endTime, text, textAlignment, line, lineType, lineAnchor,
|
||||||
position, positionAnchor, width);
|
position, positionAnchor, width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder setStartTime(long time) {
|
public Builder setStartTime(long time) {
|
||||||
startTime = time;
|
startTime = time;
|
||||||
return this;
|
return this;
|
||||||
@ -112,7 +145,7 @@ import android.util.Log;
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder setText(CharSequence aText) {
|
public Builder setText(SpannableStringBuilder aText) {
|
||||||
text = aText;
|
text = aText;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -175,6 +208,54 @@ import android.util.Log;
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void maybeApplyStyleToText(WebvttCssStyle style, int start, int end) {
|
||||||
|
if (style == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) {
|
||||||
|
text.setSpan(new StyleSpan(style.getStyle()), start, end,
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
if (style.isLinethrough()) {
|
||||||
|
text.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
if (style.isUnderline()) {
|
||||||
|
text.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
if (style.hasFontColor()) {
|
||||||
|
text.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end,
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
if (style.hasBackgroundColor()) {
|
||||||
|
text.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end,
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
if (style.getFontFamily() != null) {
|
||||||
|
text.setSpan(new TypefaceSpan(style.getFontFamily()), start, end,
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
if (style.getTextAlign() != null) {
|
||||||
|
text.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end,
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
if (style.getFontSizeUnit() != WebvttCssStyle.UNSPECIFIED) {
|
||||||
|
switch (style.getFontSizeUnit()) {
|
||||||
|
case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL:
|
||||||
|
text.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end,
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
break;
|
||||||
|
case WebvttCssStyle.FONT_SIZE_UNIT_EM:
|
||||||
|
text.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end,
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
break;
|
||||||
|
case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT:
|
||||||
|
text.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end,
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -33,12 +33,11 @@ import java.util.regex.Pattern;
|
|||||||
/**
|
/**
|
||||||
* Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues)
|
* Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues)
|
||||||
*/
|
*/
|
||||||
public final class WebvttCueParser {
|
/* package */ final class WebvttCueParser {
|
||||||
|
|
||||||
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+)(.*)?$");
|
||||||
|
|
||||||
private static final Pattern COMMENT = Pattern.compile("^NOTE((\u0020|\u0009).*)?$");
|
|
||||||
private static final Pattern CUE_SETTING_PATTERN = Pattern.compile("(\\S+?):(\\S+)");
|
private static final Pattern CUE_SETTING_PATTERN = Pattern.compile("(\\S+?):(\\S+)");
|
||||||
|
|
||||||
private static final char CHAR_LESS_THAN = '<';
|
private static final char CHAR_LESS_THAN = '<';
|
||||||
@ -79,11 +78,20 @@ public final class WebvttCueParser {
|
|||||||
* @param builder Builder for WebVTT Cues.
|
* @param builder Builder for WebVTT Cues.
|
||||||
* @return True if a valid Cue was found, false otherwise.
|
* @return True if a valid Cue was found, false otherwise.
|
||||||
*/
|
*/
|
||||||
/* package */ boolean parseNextValidCue(ParsableByteArray webvttData, WebvttCue.Builder builder) {
|
/* package */ boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder) {
|
||||||
Matcher cueHeaderMatcher;
|
String firstLine = webvttData.readLine();
|
||||||
while ((cueHeaderMatcher = findNextCueHeader(webvttData)) != null) {
|
Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(firstLine);
|
||||||
if (parseCue(cueHeaderMatcher, webvttData, builder, textBuilder)) {
|
if (cueHeaderMatcher.matches()) {
|
||||||
return true;
|
// We have found the timestamps in the first line. No id present.
|
||||||
|
return parseCue(cueHeaderMatcher, webvttData, builder, textBuilder);
|
||||||
|
} else {
|
||||||
|
// The first line is not the timestamps, but could be the cue id.
|
||||||
|
String secondLine = webvttData.readLine();
|
||||||
|
cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine);
|
||||||
|
if (cueHeaderMatcher.matches()) {
|
||||||
|
// We can do the rest of the parsing, including the id.
|
||||||
|
builder.setId(firstLine.trim());
|
||||||
|
return parseCue(cueHeaderMatcher, webvttData, builder, textBuilder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -120,30 +128,6 @@ public final class WebvttCueParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads lines up to and including the next WebVTT cue header.
|
|
||||||
*
|
|
||||||
* @param input The input from which lines should be read.
|
|
||||||
* @return A {@link Matcher} for the WebVTT cue header, or null if the end of the input was
|
|
||||||
* reached without a cue header being found. In the case that a cue header is found, groups 1,
|
|
||||||
* 2 and 3 of the returned matcher contain the start time, end time and settings list.
|
|
||||||
*/
|
|
||||||
public static Matcher findNextCueHeader(ParsableByteArray input) {
|
|
||||||
String line;
|
|
||||||
while ((line = input.readLine()) != null) {
|
|
||||||
if (COMMENT.matcher(line).matches()) {
|
|
||||||
// Skip until the end of the comment block.
|
|
||||||
while ((line = input.readLine()) != null && !line.isEmpty()) {}
|
|
||||||
} else {
|
|
||||||
Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(line);
|
|
||||||
if (cueHeaderMatcher.matches()) {
|
|
||||||
return cueHeaderMatcher;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the text payload of a WebVTT Cue and applies modifications on {@link WebvttCue.Builder}.
|
* Parses the text payload of a WebVTT Cue and applies modifications on {@link WebvttCue.Builder}.
|
||||||
*
|
*
|
||||||
|
@ -22,6 +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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple WebVTT parser.
|
* A simple WebVTT parser.
|
||||||
@ -30,32 +31,88 @@ import java.util.ArrayList;
|
|||||||
*/
|
*/
|
||||||
public final class WebvttParser extends SubtitleParser {
|
public final class WebvttParser extends SubtitleParser {
|
||||||
|
|
||||||
|
private static final int NO_EVENT_FOUND = -1;
|
||||||
|
private static final int END_OF_FILE_FOUND = 0;
|
||||||
|
private static final int COMMENT_FOUND = 1;
|
||||||
|
private static final int STYLE_BLOCK_FOUND = 2;
|
||||||
|
private static final int CUE_FOUND = 3;
|
||||||
|
private static final String COMMENT_START = "NOTE";
|
||||||
|
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 HashMap<String, WebvttCssStyle> styleMap;
|
||||||
|
|
||||||
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();
|
||||||
|
styleMap = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected final 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);
|
||||||
webvttCueBuilder.reset(); // In case a previous parse run failed with a ParserException.
|
// Initialization for consistent starting state.
|
||||||
|
webvttCueBuilder.reset();
|
||||||
|
styleMap.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())) {}
|
||||||
|
|
||||||
// Extract Cues
|
int eventFound;
|
||||||
ArrayList<WebvttCue> subtitles = new ArrayList<>();
|
ArrayList<WebvttCue> subtitles = new ArrayList<>();
|
||||||
while (cueParser.parseNextValidCue(parsableWebvttData, webvttCueBuilder)) {
|
while ((eventFound = getNextEvent(parsableWebvttData)) != END_OF_FILE_FOUND) {
|
||||||
subtitles.add(webvttCueBuilder.build());
|
if (eventFound == COMMENT_FOUND) {
|
||||||
|
skipComment(parsableWebvttData);
|
||||||
|
} else if (eventFound == STYLE_BLOCK_FOUND) {
|
||||||
|
if (!subtitles.isEmpty()) {
|
||||||
|
throw new ParserException("A style block was found after the first cue.");
|
||||||
|
}
|
||||||
|
parsableWebvttData.readLine(); // Consume the "STYLE" header.
|
||||||
|
cssParser.parseBlock(parsableWebvttData, styleMap);
|
||||||
|
} else if (eventFound == CUE_FOUND) {
|
||||||
|
if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder)) {
|
||||||
|
subtitles.add(webvttCueBuilder.build(styleMap));
|
||||||
webvttCueBuilder.reset();
|
webvttCueBuilder.reset();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
|
* consume any data from such event, if any.
|
||||||
|
*
|
||||||
|
* @return The kind of event found.
|
||||||
|
*/
|
||||||
|
private static int getNextEvent(ParsableByteArray parsableWebvttData) {
|
||||||
|
int foundEvent = NO_EVENT_FOUND;
|
||||||
|
int currentInputPosition = 0;
|
||||||
|
while (foundEvent == NO_EVENT_FOUND) {
|
||||||
|
currentInputPosition = parsableWebvttData.getPosition();
|
||||||
|
String line = parsableWebvttData.readLine();
|
||||||
|
if (line == null) {
|
||||||
|
foundEvent = END_OF_FILE_FOUND;
|
||||||
|
} else if (STYLE_START.equals(line)) {
|
||||||
|
foundEvent = STYLE_BLOCK_FOUND;
|
||||||
|
} else if (COMMENT_START.startsWith(line)) {
|
||||||
|
foundEvent = COMMENT_FOUND;
|
||||||
|
} else {
|
||||||
|
foundEvent = CUE_FOUND;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parsableWebvttData.setPosition(currentInputPosition);
|
||||||
|
return foundEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void skipComment(ParsableByteArray parsableWebvttData) {
|
||||||
|
while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer.text.webvtt;
|
|||||||
import com.google.android.exoplayer.ParserException;
|
import com.google.android.exoplayer.ParserException;
|
||||||
import com.google.android.exoplayer.util.ParsableByteArray;
|
import com.google.android.exoplayer.util.ParsableByteArray;
|
||||||
|
|
||||||
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,6 +26,7 @@ import java.util.regex.Pattern;
|
|||||||
*/
|
*/
|
||||||
public final class WebvttParserUtil {
|
public final class WebvttParserUtil {
|
||||||
|
|
||||||
|
private static final Pattern COMMENT = Pattern.compile("^NOTE((\u0020|\u0009).*)?$");
|
||||||
private static final Pattern HEADER = Pattern.compile("^\uFEFF?WEBVTT((\u0020|\u0009).*)?$");
|
private static final Pattern HEADER = Pattern.compile("^\uFEFF?WEBVTT((\u0020|\u0009).*)?$");
|
||||||
|
|
||||||
private WebvttParserUtil() {}
|
private WebvttParserUtil() {}
|
||||||
@ -72,4 +74,28 @@ public final class WebvttParserUtil {
|
|||||||
return Float.parseFloat(s.substring(0, s.length() - 1)) / 100;
|
return Float.parseFloat(s.substring(0, s.length() - 1)) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads lines up to and including the next WebVTT cue header.
|
||||||
|
*
|
||||||
|
* @param input The input from which lines should be read.
|
||||||
|
* @return A {@link Matcher} for the WebVTT cue header, or null if the end of the input was
|
||||||
|
* reached without a cue header being found. In the case that a cue header is found, groups 1,
|
||||||
|
* 2 and 3 of the returned matcher contain the start time, end time and settings list.
|
||||||
|
*/
|
||||||
|
public static Matcher findNextCueHeader(ParsableByteArray input) {
|
||||||
|
String line;
|
||||||
|
while ((line = input.readLine()) != null) {
|
||||||
|
if (COMMENT.matcher(line).matches()) {
|
||||||
|
// Skip until the end of the comment block.
|
||||||
|
while ((line = input.readLine()) != null && !line.isEmpty()) {}
|
||||||
|
} else {
|
||||||
|
Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(line);
|
||||||
|
if (cueHeaderMatcher.matches()) {
|
||||||
|
return cueHeaderMatcher;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user