From 51df2dce46473ebd56f79225c3bb079e5bced7c5 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 13 Apr 2016 08:19:38 -0700 Subject: [PATCH] Add support for voice selection in WebVTT CSS Allow styling Hello with ::cue(v[voice="Someone"]) { ... }. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=119748009 --- .../androidTest/assets/webvtt/with_css_styles | 12 +++- .../text/webvtt/WebvttParserTest.java | 9 ++- .../text/webvtt/WebvttCueParser.java | 55 +++++++++++-------- 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/library/src/androidTest/assets/webvtt/with_css_styles b/library/src/androidTest/assets/webvtt/with_css_styles index 12c8422b9b..2056d50adf 100644 --- a/library/src/androidTest/assets/webvtt/with_css_styles +++ b/library/src/androidTest/assets/webvtt/with_css_styles @@ -14,6 +14,12 @@ STYLE color: peachpuff; } +STYLE +::cue(v[voice="LaGord"]) { background-color: lime } + +STYLE +::cue(v[voice="The Frog"]) { font-weight: bold } + STYLE ::cue(v){text-decoration:underline} @@ -26,4 +32,8 @@ id2 This is the second subtitle. 00:20.000 --> 00:21.000 -This is a reference by element +This is a reference by element + +00:25.000 --> 00:28.000 +You are an idiot +You don't have the guts 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 1597062dc8..445988918f 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 @@ -19,11 +19,13 @@ import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.testutil.TestUtil; import com.google.android.exoplayer.text.Cue; +import android.graphics.Typeface; import android.test.InstrumentationTestCase; import android.text.Layout.Alignment; import android.text.Spanned; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; import java.io.IOException; @@ -155,7 +157,7 @@ public class WebvttParserTest extends InstrumentationTestCase { WebvttSubtitle subtitle = parser.decode(bytes, bytes.length); // Test event count. - assertEquals(6, subtitle.getEventTimeCount()); + assertEquals(8, subtitle.getEventTimeCount()); // Test cues. assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle."); @@ -164,13 +166,18 @@ 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); + Cue cue4 = subtitle.getCues(25000000).get(0); 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(), BackgroundColorSpan.class).length); assertEquals(2, s2.getSpans(0, s2.length(), ForegroundColorSpan.class).length); assertEquals(1, s3.getSpans(10, s3.length(), UnderlineSpan.class).length); + assertEquals(2, s4.getSpans(0, 16, BackgroundColorSpan.class).length); + assertEquals(1, s4.getSpans(17, s4.length(), StyleSpan.class).length); + assertEquals(Typeface.BOLD, s4.getSpans(17, s4.length(), StyleSpan.class)[0].getStyle()); } private static void assertCue(WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs, 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 6c08126a5b..60a942358a 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 @@ -45,7 +45,6 @@ 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+)(.*)?$"); @@ -57,7 +56,6 @@ import java.util.regex.Pattern; private static final char CHAR_AMPERSAND = '&'; private static final char CHAR_SEMI_COLON = ';'; private static final char CHAR_SPACE = ' '; - private static final String SPACE = " "; private static final String ENTITY_LESS_THAN = "lt"; private static final String ENTITY_GREATER_THAN = "gt"; @@ -70,6 +68,10 @@ import java.util.regex.Pattern; private static final String TAG_CLASS = "c"; private static final String TAG_VOICE = "v"; 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_ITALIC = Typeface.ITALIC; @@ -153,7 +155,6 @@ import java.util.regex.Pattern; Map styleMap) { SpannableStringBuilder spannedText = new SpannableStringBuilder(); Stack startTagStack = new Stack<>(); - String[] tagTokens; int pos = 0; while (pos < markup.length()) { char curr = markup.charAt(pos); @@ -167,10 +168,10 @@ import java.util.regex.Pattern; boolean isClosingTag = markup.charAt(ltPos + 1) == CHAR_SLASH; pos = findEndOfTag(markup, ltPos + 1); boolean isVoidTag = markup.charAt(pos - 2) == CHAR_SLASH; - - tagTokens = tokenizeTag(markup.substring( - ltPos + (isClosingTag ? 2 : 1), isVoidTag ? pos - 2 : pos - 1)); - if (tagTokens == null || !isSupportedTag(tagTokens[0])) { + String fullTagExpression = markup.substring(ltPos + (isClosingTag ? 2 : 1), + isVoidTag ? pos - 2 : pos - 1); + String tagName = getTagName(fullTagExpression); + if (tagName == null || !isSupportedTag(tagName)) { continue; } if (isClosingTag) { @@ -181,9 +182,10 @@ import java.util.regex.Pattern; } startTag = startTagStack.pop(); applySpansForTag(startTag, spannedText, styleMap); - } while(!startTag.name.equals(tagTokens[0])); + } while(!startTag.name.equals(tagName)); } else if (!isVoidTag) { - startTagStack.push(new StartTag(tagTokens[0], spannedText.length())); + startTagStack.push(new StartTag(tagName, spannedText.length(), + TAG_VOICE.equals(tagName) ? getVoiceName(fullTagExpression) : null)); } break; case CHAR_AMPERSAND: @@ -309,8 +311,8 @@ import java.util.regex.Pattern; * Find end of tag (>). The position returned is the position of the > plus one (exclusive). * * @param markup The WebVTT cue markup to be parsed. - * @param startPos the position from where to start searching for the end of tag. - * @return the position of the end of tag plus 1 (one). + * @param startPos The position from where to start searching for the end of tag. + * @return The position of the end of tag plus 1 (one). */ private static int findEndOfTag(String markup, int startPos) { int idx = markup.indexOf(CHAR_GREATER_THAN, startPos); @@ -376,6 +378,11 @@ import java.util.regex.Pattern; return; } applyStyleToText(spannedText, styleForTag, start, end); + if (startTag.voiceName != null) { + WebvttCssStyle styleForVoice = styleMap.get(CUE_VOICE_PREFIX + startTag.voiceName + + CUE_VOICE_SUFFIX); + applyStyleToText(spannedText, styleForVoice, start, end); + } } private static void applyStyleToText(SpannableStringBuilder spannedText, @@ -428,31 +435,33 @@ import java.util.regex.Pattern; } /** - * Tokenizes a tag expression into tag name (pos 0) and classes (pos 1..n). + * Gets the tag name for the given tag contents. * - * @param fullTagExpression characters between &lt: and &gt; of a start or end tag - * @return an array of Strings with the tag name at pos 0 followed by style classes - * or null if it's an empty tag: '<>' + * @param tagExpression Characters between &lt: and &gt; of a start or end tag. + * @return The name of tag. */ - private static String[] tokenizeTag(String fullTagExpression) { - fullTagExpression = fullTagExpression.replace("\\s+", " ").trim(); - if (fullTagExpression.length() == 0) { + private static String getTagName(String tagExpression) { + tagExpression = tagExpression.trim(); + if (tagExpression.isEmpty()) { return null; } - if (fullTagExpression.contains(SPACE)) { - fullTagExpression = fullTagExpression.substring(0, fullTagExpression.indexOf(SPACE)); - } - return fullTagExpression.split("\\."); + return tagExpression.split("[ \\.]")[0]; + } + + private static String getVoiceName(String fullTagExpression) { + return fullTagExpression.trim().substring(fullTagExpression.indexOf(" ")).trim(); } private static final class StartTag { public final String name; public final int position; + public final String voiceName; - public StartTag(String name, int position) { + public StartTag(String name, int position, String voiceName) { this.position = position; this.name = name; + this.voiceName = voiceName; } }