Add support for voice selection in WebVTT CSS

Allow styling <v Someone>Hello</v> with ::cue(v[voice="Someone"]) { ... }.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=119748009
This commit is contained in:
aquilescanta 2016-04-13 08:19:38 -07:00 committed by Oliver Woodman
parent e594eccd4d
commit 51df2dce46
3 changed files with 51 additions and 25 deletions

View file

@ -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 <v Mary>reference by element</v>
This is a <v.clazz Mary>reference by element</v>
00:25.000 --> 00:28.000
<v LaGord>You are an idiot</v>
<v The Frog>You don't have the guts</v>

View file

@ -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,

View file

@ -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<String, WebvttCssStyle> styleMap) {
SpannableStringBuilder spannedText = new SpannableStringBuilder();
Stack<StartTag> 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 (&gt;). The position returned is the position of the &gt; 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 &amp;lt: and &amp;gt; of a start or end tag
* @return an array of <code>String</code>s with the tag name at pos 0 followed by style classes
* or null if it's an empty tag: '&lt;&gt;'
* @param tagExpression Characters between &amp;lt: and &amp;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;
}
}