emptyMap());
return (Spanned) builder.build().text;
}
diff --git a/library/src/androidTest/java/com/google/android/exoplayer/text/webvtt/WebvttParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer/text/webvtt/WebvttParserTest.java
index 0d6152bdc5..1597062dc8 100644
--- a/library/src/androidTest/java/com/google/android/exoplayer/text/webvtt/WebvttParserTest.java
+++ b/library/src/androidTest/java/com/google/android/exoplayer/text/webvtt/WebvttParserTest.java
@@ -24,6 +24,7 @@ import android.text.Layout.Alignment;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
+import android.text.style.UnderlineSpan;
import java.io.IOException;
import java.util.List;
@@ -154,7 +155,7 @@ public class WebvttParserTest extends InstrumentationTestCase {
WebvttSubtitle subtitle = parser.decode(bytes, bytes.length);
// Test event count.
- assertEquals(4, subtitle.getEventTimeCount());
+ assertEquals(6, subtitle.getEventTimeCount());
// Test cues.
assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
@@ -162,11 +163,14 @@ public class WebvttParserTest extends InstrumentationTestCase {
Cue cue1 = subtitle.getCues(0).get(0);
Cue cue2 = subtitle.getCues(2345000).get(0);
+ Cue cue3 = subtitle.getCues(20000000).get(0);
Spanned s1 = (Spanned) cue1.text;
Spanned s2 = (Spanned) cue2.text;
+ Spanned s3 = (Spanned) cue3.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);
+ assertEquals(1, s3.getSpans(10, s3.length(), UnderlineSpan.class).length);
}
private static void assertCue(WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs,
diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/CssParser.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/CssParser.java
index 8573d72dd8..818c720afe 100644
--- a/library/src/main/java/com/google/android/exoplayer/text/webvtt/CssParser.java
+++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/CssParser.java
@@ -86,20 +86,22 @@ import java.util.Map;
}
/**
- * Returns a string containing the selector. Empty string is the universal selector, and null
- * means syntax error.
+ * Returns a string containing the selector. {@link WebvttCueParser#UNIVERSAL_CUE_ID} is the
+ * universal selector, and null means syntax error.
*
- * Expected inputs are:
- * ::cue
- * ::cue(#id)
- * ::cue(elem)
- * ::cue(.class)
- * ::cue(elem.class)
- * ::cue(v[voice="Someone"])
+ *
Expected inputs are:
+ *
+ * - ::cue
+ *
- ::cue(#id)
+ *
- ::cue(elem)
+ *
- ::cue(.class)
+ *
- ::cue(elem.class)
+ *
- ::cue(v[voice="Someone"])
+ *
*
* @param input From which the selector is obtained.
- * @return A string containing the target, {@link WebvttCue#UNIVERSAL_CUE_ID} if targets all cues
- * and null if an error was encountered.
+ * @return A string containing the target, {@link WebvttCueParser#UNIVERSAL_CUE_ID} if the
+ * selector is universal (targets all cues) or null if an error was encountered.
*/
private static String parseSelector(ParsableByteArray input, StringBuilder stringBuilder) {
skipWhitespaceAndComments(input);
@@ -117,7 +119,7 @@ import java.util.Map;
}
if ("{".equals(token)) {
input.setPosition(position);
- return WebvttCue.UNIVERSAL_CUE_ID;
+ return WebvttCueParser.UNIVERSAL_CUE_ID;
}
String target = null;
if ("(".equals(token)) {
diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/Mp4WebvttParser.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/Mp4WebvttParser.java
index a8e49070ee..4b86a97ab7 100644
--- a/library/src/main/java/com/google/android/exoplayer/text/webvtt/Mp4WebvttParser.java
+++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/Mp4WebvttParser.java
@@ -22,6 +22,7 @@ import com.google.android.exoplayer.util.ParsableByteArray;
import com.google.android.exoplayer.util.Util;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
/**
@@ -82,7 +83,8 @@ public final class Mp4WebvttParser extends SubtitleParser {
if (boxType == TYPE_sttg) {
WebvttCueParser.parseCueSettingsList(boxPayload, builder);
} else if (boxType == TYPE_payl) {
- WebvttCueParser.parseCueText(boxPayload.trim(), builder);
+ WebvttCueParser.parseCueText(null, boxPayload.trim(), builder,
+ Collections.emptyMap());
} else {
// Other VTTCueBox children are still not supported and are ignored.
}
diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttCue.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttCue.java
index bb0d87b7ec..7ca8c0002d 100644
--- a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttCue.java
+++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttCue.java
@@ -18,32 +18,14 @@ package com.google.android.exoplayer.text.webvtt;
import com.google.android.exoplayer.text.Cue;
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 java.util.Collections;
-import java.util.Map;
-
/**
* A representation of a WebVTT cue.
*/
/* package */ final class WebvttCue extends Cue {
- public static final String UNIVERSAL_CUE_ID = "";
- public static final String CUE_ID_PREFIX = "#";
-
- public final String id;
public final long startTime;
public final long endTime;
@@ -52,15 +34,13 @@ import java.util.Map;
}
public WebvttCue(long startTime, long endTime, CharSequence text) {
- this(null, startTime, endTime, text, null, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET,
+ this(startTime, endTime, text, null, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET,
Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
}
- public WebvttCue(String id, long startTime, long endTime, CharSequence text,
- Alignment textAlignment, float line, int lineType, int lineAnchor, float position,
- int positionAnchor, float width) {
+ public WebvttCue(long startTime, long endTime, CharSequence text, Alignment textAlignment,
+ float line, int lineType, int lineAnchor, float position, int positionAnchor, float width) {
super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width);
- this.id = id;
this.startTime = startTime;
this.endTime = endTime;
}
@@ -83,7 +63,6 @@ import java.util.Map;
private static final String TAG = "WebvttCueBuilder";
- private String id;
private long startTime;
private long endTime;
private SpannableStringBuilder text;
@@ -117,25 +96,13 @@ import java.util.Map;
// Construction methods.
public WebvttCue build() {
- return build(Collections.emptyMap());
- }
-
- public WebvttCue build(Map styleMap) {
- // TODO: Add support for inner spans.
- maybeApplyStyleToText(styleMap.get(UNIVERSAL_CUE_ID), 0, text.length());
- maybeApplyStyleToText(styleMap.get(CUE_ID_PREFIX + id), 0, text.length());
if (position != Cue.DIMEN_UNSET && positionAnchor == Cue.TYPE_UNSET) {
derivePositionAnchorFromAlignment();
}
- return new WebvttCue(id, startTime, endTime, text, textAlignment, line, lineType, lineAnchor,
+ return new WebvttCue(startTime, endTime, text, textAlignment, line, lineType, lineAnchor,
position, positionAnchor, width);
}
- public Builder setId(String id) {
- this.id = id;
- return this;
- }
-
public Builder setStartTime(long time) {
startTime = time;
return this;
@@ -209,54 +176,6 @@ import java.util.Map;
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;
- }
- }
- }
-
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttCueParser.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttCueParser.java
index 2635a2ee8c..6c08126a5b 100644
--- a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttCueParser.java
+++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttCueParser.java
@@ -20,12 +20,21 @@ import com.google.android.exoplayer.util.ParsableByteArray;
import android.graphics.Typeface;
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 java.util.Map;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -35,6 +44,8 @@ import java.util.regex.Pattern;
*/
/* package */ final class WebvttCueParser {
+ public static final String UNIVERSAL_CUE_ID = "";
+ public static final String CUE_ID_PREFIX = "#";
public static final Pattern CUE_HEADER_PATTERN = Pattern
.compile("^(\\S+)\\s+-->\\s+(\\S+)(.*)?$");
@@ -76,22 +87,24 @@ import java.util.regex.Pattern;
*
* @param webvttData Parsable WebVTT file data.
* @param builder Builder for WebVTT Cues.
+ * @param styleMap Maps selector to style as referenced by the CSS ::cue pseudo-element.
* @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 styleMap) {
String firstLine = webvttData.readLine();
Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(firstLine);
if (cueHeaderMatcher.matches()) {
// We have found the timestamps in the first line. No id present.
- return parseCue(cueHeaderMatcher, webvttData, builder, textBuilder);
+ return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styleMap);
} 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 parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder,
+ styleMap);
}
}
return false;
@@ -131,10 +144,13 @@ import java.util.regex.Pattern;
/**
* Parses the text payload of a WebVTT Cue and applies modifications on {@link WebvttCue.Builder}.
*
+ * @param id Id of the cue, {@code null} if it is not present.
* @param markup The markup text to be parsed.
+ * @param styleMap Maps selector to style as referenced by the CSS ::cue pseudo-element.
* @param builder Target builder.
*/
- /* package */ static void parseCueText(String markup, WebvttCue.Builder builder) {
+ /* package */ static void parseCueText(String id, String markup, WebvttCue.Builder builder,
+ Map styleMap) {
SpannableStringBuilder spannedText = new SpannableStringBuilder();
Stack startTagStack = new Stack<>();
String[] tagTokens;
@@ -164,7 +180,7 @@ import java.util.regex.Pattern;
break;
}
startTag = startTagStack.pop();
- applySpansForTag(startTag, spannedText);
+ applySpansForTag(startTag, spannedText, styleMap);
} while(!startTag.name.equals(tagTokens[0]));
} else if (!isVoidTag) {
startTagStack.push(new StartTag(tagTokens[0], spannedText.length()));
@@ -194,13 +210,15 @@ import java.util.regex.Pattern;
}
// apply unclosed tags
while (!startTagStack.isEmpty()) {
- applySpansForTag(startTagStack.pop(), spannedText);
+ applySpansForTag(startTagStack.pop(), spannedText, styleMap);
}
+ applyStyleToText(spannedText, styleMap.get(UNIVERSAL_CUE_ID), 0, spannedText.length());
+ applyStyleToText(spannedText, styleMap.get(CUE_ID_PREFIX + id), 0, spannedText.length());
builder.setText(spannedText);
}
- private static boolean parseCue(Matcher cueHeaderMatcher, ParsableByteArray webvttData,
- WebvttCue.Builder builder, StringBuilder textBuilder) {
+ private static boolean parseCue(String id, Matcher cueHeaderMatcher, ParsableByteArray webvttData,
+ WebvttCue.Builder builder, StringBuilder textBuilder, Map styleMap) {
try {
// Parse the cue start and end times.
builder.setStartTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)))
@@ -221,7 +239,7 @@ import java.util.regex.Pattern;
}
textBuilder.append(line.trim());
}
- parseCueText(textBuilder.toString(), builder);
+ parseCueText(id, textBuilder.toString(), builder, styleMap);
return true;
}
@@ -333,22 +351,79 @@ import java.util.regex.Pattern;
}
}
- private static void applySpansForTag(StartTag startTag, SpannableStringBuilder spannedText) {
+ private static void applySpansForTag(StartTag startTag, SpannableStringBuilder spannedText,
+ Map styleMap) {
+ WebvttCssStyle styleForTag = styleMap.get(startTag.name);
+ int start = startTag.position;
+ int end = spannedText.length();
switch(startTag.name) {
case TAG_BOLD:
- spannedText.setSpan(new StyleSpan(STYLE_BOLD), startTag.position,
- spannedText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- return;
- case TAG_ITALIC:
- spannedText.setSpan(new StyleSpan(STYLE_ITALIC), startTag.position,
- spannedText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- return;
- case TAG_UNDERLINE:
- spannedText.setSpan(new UnderlineSpan(), startTag.position,
- spannedText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- return;
- default:
+ spannedText.setSpan(new StyleSpan(STYLE_BOLD), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break;
+ case TAG_ITALIC:
+ spannedText.setSpan(new StyleSpan(STYLE_ITALIC), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TAG_UNDERLINE:
+ spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case TAG_CLASS:
+ case TAG_LANG:
+ case TAG_VOICE:
+ break;
+ default:
+ return;
+ }
+ applyStyleToText(spannedText, styleForTag, start, end);
+ }
+
+ private static void applyStyleToText(SpannableStringBuilder spannedText,
+ WebvttCssStyle style, int start, int end) {
+ if (style == null) {
+ return;
+ }
+ if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) {
+ spannedText.setSpan(new StyleSpan(style.getStyle()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.isLinethrough()) {
+ spannedText.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.isUnderline()) {
+ spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.hasFontColor()) {
+ spannedText.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.hasBackgroundColor()) {
+ spannedText.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.getFontFamily() != null) {
+ spannedText.setSpan(new TypefaceSpan(style.getFontFamily()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ if (style.getTextAlign() != null) {
+ spannedText.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:
+ spannedText.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case WebvttCssStyle.FONT_SIZE_UNIT_EM:
+ spannedText.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT:
+ spannedText.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ break;
+ }
}
}
diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java
index d7b5c9fbbc..5586af0f90 100644
--- a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java
+++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParser.java
@@ -76,8 +76,8 @@ public final class WebvttParser extends SubtitleParser {
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));
+ if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, styleMap)) {
+ subtitles.add(webvttCueBuilder.build());
webvttCueBuilder.reset();
}
}