Add support for Cue Settings and Spanned text in MP4WebVTT

Using the provided methods by the previous refactors, it is now possible to use all of the WebVTT features already available.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=112243172
This commit is contained in:
aquilescanta 2016-01-15 06:11:44 -08:00 committed by Oliver Woodman
parent d45f0b8b6d
commit 5baf55176b
5 changed files with 98 additions and 102 deletions

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.text.mp4webvtt;
package com.google.android.exoplayer.text.webvtt;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.text.Cue;
@ -73,16 +73,6 @@ public final class Mp4WebvttParserTest extends TestCase {
0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64 // Hello World
};
private static final byte[] NO_PAYLOAD_CUE_SAMPLE = {
0x00, 0x00, 0x00, 0x1B, // Size
0x76, 0x74, 0x74, 0x63, // Box type. First VTT Cue box begins:
0x00, 0x00, 0x00, 0x13, // First contained payload box's size
0x71, 0x61, 0x79, 0x6c, // Type of box, which is not payload (qayl)
0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, // Hello World
};
private static final byte[] INCOMPLETE_HEADER_SAMPLE = {
0x00, 0x00, 0x00, 0x23, // Size
0x76, 0x74, 0x74, 0x63, // "vttc" Box type. VTT Cue box begins:
@ -96,7 +86,6 @@ public final class Mp4WebvttParserTest extends TestCase {
0x76, 0x74, 0x74
};
private Mp4WebvttParser parser;
@Override
@ -126,16 +115,6 @@ public final class Mp4WebvttParserTest extends TestCase {
// Negative tests.
public void testSampleWithVttCueWithNoPayload() {
try {
parser.parse(NO_PAYLOAD_CUE_SAMPLE, 0, NO_PAYLOAD_CUE_SAMPLE.length);
} catch (ParserException e) {
// Expected.
return;
}
fail("The parser should have failed, no payload was included in the VTTCue.");
}
public void testSampleWithIncompleteHeader() {
try {
parser.parse(INCOMPLETE_HEADER_SAMPLE, 0, INCOMPLETE_HEADER_SAMPLE.length);
@ -193,8 +172,8 @@ public final class Mp4WebvttParserTest extends TestCase {
if (aCue.size != anotherCue.size) {
differences.add("size: " + aCue.size + " | " + anotherCue.size);
}
if (!Util.areEqual(aCue.text, anotherCue.text)) {
differences.add("text: " + aCue.text + " | " + anotherCue.text);
if (!Util.areEqual(aCue.text.toString(), anotherCue.text.toString())) {
differences.add("text: '" + aCue.text + "' | '" + anotherCue.text + '\'');
}
if (!Util.areEqual(aCue.textAlignment, anotherCue.textAlignment)) {
differences.add("textAlignment: " + aCue.textAlignment + " | " + anotherCue.textAlignment);

View File

@ -27,7 +27,7 @@ import android.text.style.UnderlineSpan;
public final class WebvttCueParserTest extends InstrumentationTestCase {
public void testParseStrictValidClassesAndTrailingTokens() throws Exception {
Spanned text = WebvttCueParser.parseCueText("<v.first.loud Esme>"
Spanned text = parseCueText("<v.first.loud Esme>"
+ "This <u.style1.style2 some stuff>is</u> text with <b.foo><i.bar>html</i></b> tags");
assertEquals("This is text with html tags", text.toString());
@ -48,18 +48,16 @@ public final class WebvttCueParserTest extends InstrumentationTestCase {
}
public void testParseStrictValidUnsupportedTagsStrippedOut() throws Exception {
Spanned text = WebvttCueParser.parseCueText(
"<v.first.loud Esme>This <unsupported>is</unsupported> text with "
Spanned text = parseCueText("<v.first.loud Esme>This <unsupported>is</unsupported> text with "
+ "<notsupp><invalid>html</invalid></notsupp> tags");
assertEquals("This is text with html tags", text.toString());
assertEquals(0, getSpans(text, UnderlineSpan.class).length);
assertEquals(0, getSpans(text, StyleSpan.class).length);
}
public void testParseWellFormedUnclosedEndAtCueEnd() throws Exception {
Spanned text = WebvttCueParser.parseCueText(
"An <u some trailing stuff>unclosed u tag with <i>italic</i> inside");
Spanned text = parseCueText("An <u some trailing stuff>unclosed u tag with "
+ "<i>italic</i> inside");
assertEquals("An unclosed u tag with italic inside", text.toString());
@ -76,8 +74,7 @@ public final class WebvttCueParserTest extends InstrumentationTestCase {
}
public void testParseWellFormedUnclosedEndAtParent() throws Exception {
Spanned text = WebvttCueParser.parseCueText(
"An unclosed u tag with <i><u>underline and italic</i> inside");
Spanned text = parseCueText("An unclosed u tag with <i><u>underline and italic</i> inside");
assertEquals("An unclosed u tag with underline and italic inside", text.toString());
@ -95,8 +92,7 @@ public final class WebvttCueParserTest extends InstrumentationTestCase {
}
public void testParseMalformedNestedElements() throws Exception {
Spanned text = WebvttCueParser.parseCueText(
"<b><u>An unclosed u tag with <i>italic</u> inside</i></b>");
Spanned text = parseCueText("<b><u>An unclosed u tag with <i>italic</u> inside</i></b>");
assertEquals("An unclosed u tag with italic inside", text.toString());
UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class);
@ -121,7 +117,7 @@ public final class WebvttCueParserTest extends InstrumentationTestCase {
}
public void testParseCloseNonExistingTag() throws Exception {
Spanned text = WebvttCueParser.parseCueText("blah<b>blah</i>blah</b>blah");
Spanned text = parseCueText("blah<b>blah</i>blah</b>blah");
assertEquals("blahblahblahblah", text.toString());
StyleSpan[] spans = getSpans(text, StyleSpan.class);
@ -132,42 +128,42 @@ public final class WebvttCueParserTest extends InstrumentationTestCase {
}
public void testParseEmptyTagName() throws Exception {
Spanned text = WebvttCueParser.parseCueText("An unclosed u tag with <>italic inside");
Spanned text = parseCueText("An unclosed u tag with <>italic inside");
assertEquals("An unclosed u tag with italic inside", text.toString());
}
public void testParseEntities() throws Exception {
Spanned text = WebvttCueParser.parseCueText("&amp; &gt; &lt; &nbsp;");
Spanned text = parseCueText("&amp; &gt; &lt; &nbsp;");
assertEquals("& > < ", text.toString());
}
public void testParseEntitiesUnsupported() throws Exception {
Spanned text = WebvttCueParser.parseCueText("&noway; &sure;");
Spanned text = parseCueText("&noway; &sure;");
assertEquals(" ", text.toString());
}
public void testParseEntitiesNotTerminated() throws Exception {
Spanned text = WebvttCueParser.parseCueText("&amp here comes text");
Spanned text = parseCueText("&amp here comes text");
assertEquals("& here comes text", text.toString());
}
public void testParseEntitiesNotTerminatedUnsupported() throws Exception {
Spanned text = WebvttCueParser.parseCueText("&surenot here comes text");
Spanned text = parseCueText("&surenot here comes text");
assertEquals(" here comes text", text.toString());
}
public void testParseEntitiesNotTerminatedNoSpace() throws Exception {
Spanned text = WebvttCueParser.parseCueText("&surenot");
Spanned text = parseCueText("&surenot");
assertEquals("&surenot", text.toString());
}
public void testParseVoidTag() throws Exception {
Spanned text = WebvttCueParser.parseCueText("here comes<br/> text<br/>");
Spanned text = parseCueText("here comes<br/> text<br/>");
assertEquals("here comes text", text.toString());
}
public void testParseMultipleTagsOfSameKind() {
Spanned text = WebvttCueParser.parseCueText("blah <b>blah</b> blah <b>foo</b>");
Spanned text = parseCueText("blah <b>blah</b> blah <b>foo</b>");
assertEquals("blah blah blah foo", text.toString());
StyleSpan[] spans = getSpans(text, StyleSpan.class);
@ -181,7 +177,7 @@ public final class WebvttCueParserTest extends InstrumentationTestCase {
}
public void testParseInvalidVoidSlash() {
Spanned text = WebvttCueParser.parseCueText("blah <b/.st1.st2 trailing stuff> blah");
Spanned text = parseCueText("blah <b/.st1.st2 trailing stuff> blah");
assertEquals("blah blah", text.toString());
StyleSpan[] spans = getSpans(text, StyleSpan.class);
@ -189,40 +185,46 @@ public final class WebvttCueParserTest extends InstrumentationTestCase {
}
public void testParseMonkey() throws Exception {
Spanned text = WebvttCueParser.parseCueText(
"< u>An unclosed u tag with <<<<< i>italic</u></u></u></u ></i><u><u> inside");
Spanned text = parseCueText("< u>An unclosed u tag with <<<<< i>italic</u></u></u></u >"
+ "</i><u><u> inside");
assertEquals("An unclosed u tag with italic inside", text.toString());
text = WebvttCueParser.parseCueText(">>>>>>>>>An unclosed u tag with <<<<< italic</u></u></u>"
text = parseCueText(">>>>>>>>>An unclosed u tag with <<<<< italic</u></u></u>"
+ "</u ></i><u><u> inside");
assertEquals(">>>>>>>>>An unclosed u tag with inside", text.toString());
}
public void testParseCornerCases() throws Exception {
Spanned text = WebvttCueParser.parseCueText(">");
Spanned text = parseCueText(">");
assertEquals(">", text.toString());
text = WebvttCueParser.parseCueText("<");
text = parseCueText("<");
assertEquals("", text.toString());
text = WebvttCueParser.parseCueText("<b.st1.st2 annotation");
text = parseCueText("<b.st1.st2 annotation");
assertEquals("", text.toString());
text = WebvttCueParser.parseCueText("<<<<<<<<<<<<<<<<");
text = parseCueText("<<<<<<<<<<<<<<<<");
assertEquals("", text.toString());
text = WebvttCueParser.parseCueText("<<<<<<>><<<<<<<<<<");
text = parseCueText("<<<<<<>><<<<<<<<<<");
assertEquals(">", text.toString());
text = WebvttCueParser.parseCueText("<>");
text = parseCueText("<>");
assertEquals("", text.toString());
text = WebvttCueParser.parseCueText("&");
text = parseCueText("&");
assertEquals("&", text.toString());
text = WebvttCueParser.parseCueText("&&&&&&&");
text = parseCueText("&&&&&&&");
assertEquals("&&&&&&&", text.toString());
}
private static Spanned parseCueText(String string) {
WebvttCue.Builder builder = new WebvttCue.Builder();
WebvttCueParser.parseCueText(string, builder);
return (Spanned) builder.build().text;
}
private static <T> T[] getSpans(Spanned text, Class<T> spanType) {
return text.getSpans(0, text.length(), spanType);
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.text.mp4webvtt;
package com.google.android.exoplayer.text.webvtt;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.text.Cue;
@ -32,13 +32,16 @@ public final class Mp4WebvttParser implements SubtitleParser {
private static final int BOX_HEADER_SIZE = 8;
private static final int TYPE_vttc = Util.getIntegerCodeForString("vttc");
private static final int TYPE_payl = Util.getIntegerCodeForString("payl");
private static final int TYPE_sttg = Util.getIntegerCodeForString("sttg");
private static final int TYPE_vttc = Util.getIntegerCodeForString("vttc");
private final ParsableByteArray sampleData;
private final WebvttCue.Builder builder;
public Mp4WebvttParser() {
sampleData = new ParsableByteArray();
builder = new WebvttCue.Builder();
}
@Override
@ -60,7 +63,7 @@ public final class Mp4WebvttParser implements SubtitleParser {
int boxSize = sampleData.readInt();
int boxType = sampleData.readInt();
if (boxType == TYPE_vttc) {
resultingCueList.add(parseVttCueBox(sampleData));
resultingCueList.add(parseVttCueBox(sampleData, builder, boxSize - BOX_HEADER_SIZE));
} else {
// Peers of the VTTCueBox are still not supported and are skipped.
sampleData.skipBytes(boxSize - BOX_HEADER_SIZE);
@ -69,24 +72,29 @@ public final class Mp4WebvttParser implements SubtitleParser {
return new Mp4WebvttSubtitle(resultingCueList);
}
private static Cue parseVttCueBox(ParsableByteArray sampleData) throws ParserException {
while (sampleData.bytesLeft() > 0) {
if (sampleData.bytesLeft() < BOX_HEADER_SIZE) {
private static Cue parseVttCueBox(ParsableByteArray sampleData, WebvttCue.Builder builder,
int remainingCueBoxBytes) throws ParserException {
builder.reset();
while (remainingCueBoxBytes > 0) {
if (remainingCueBoxBytes < BOX_HEADER_SIZE) {
throw new ParserException("Incomplete vtt cue box header found.");
}
int boxSize = sampleData.readInt();
int boxType = sampleData.readInt();
if (boxType == TYPE_payl) {
int payloadLength = boxSize - BOX_HEADER_SIZE;
String cueText = new String(sampleData.data, sampleData.getPosition(), payloadLength);
sampleData.skipBytes(payloadLength);
return new Cue(cueText.trim());
remainingCueBoxBytes -= BOX_HEADER_SIZE;
int payloadLength = boxSize - BOX_HEADER_SIZE;
String boxPayload = new String(sampleData.data, sampleData.getPosition(), payloadLength);
sampleData.skipBytes(payloadLength);
remainingCueBoxBytes -= payloadLength;
if (boxType == TYPE_sttg) {
WebvttCueParser.parseCueSettingsList(boxPayload, builder);
} else if (boxType == TYPE_payl) {
WebvttCueParser.parseCueText(boxPayload.trim(), builder);
} else {
// Other VTTCueBox children are still not supported and are skipped.
sampleData.skipBytes(boxSize - BOX_HEADER_SIZE);
// Other VTTCueBox children are still not supported and are ignored.
}
}
throw new ParserException("VTTCueBox does not contain mandatory payload box.");
return builder.build();
}
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.text.mp4webvtt;
package com.google.android.exoplayer.text.webvtt;
import com.google.android.exoplayer.text.Cue;
import com.google.android.exoplayer.text.Subtitle;

View File

@ -76,13 +76,13 @@ public final class WebvttCueParser {
* Parses the next valid WebVTT cue in a parsable array, including timestamps, settings and text.
*
* @param webvttData Parsable WebVTT file data.
* @param cueBuilder Builder for WebVTT Cues.
* @param builder Builder for WebVTT Cues.
* @return True if a valid Cue was found, false otherwise.
*/
public boolean parseNextValidCue(ParsableByteArray webvttData, WebvttCue.Builder cueBuilder) {
/* package */ boolean parseNextValidCue(ParsableByteArray webvttData, WebvttCue.Builder builder) {
Matcher cueHeaderMatcher;
while ((cueHeaderMatcher = findNextCueHeader(webvttData)) != null) {
if (parseCue(cueHeaderMatcher, webvttData, cueBuilder, textBuilder)) {
if (parseCue(cueHeaderMatcher, webvttData, builder, textBuilder)) {
return true;
}
}
@ -95,7 +95,8 @@ public final class WebvttCueParser {
* @param cueSettingsList String containing the settings for a given cue.
* @param builder The {@link WebvttCue.Builder} where incremental construction takes place.
*/
public static void parseCueSettingsList(String cueSettingsList, WebvttCue.Builder builder) {
/* package */ static void parseCueSettingsList(String cueSettingsList,
WebvttCue.Builder builder) {
// Parse the cue settings list.
Matcher cueSettingMatcher = CUE_SETTING_PATTERN.matcher(cueSettingsList);
while (cueSettingMatcher.find()) {
@ -143,33 +144,13 @@ public final class WebvttCueParser {
return null;
}
private static boolean parseCue(Matcher cueHeaderMatcher, ParsableByteArray webvttData,
WebvttCue.Builder builder, StringBuilder textBuilder) {
try {
// Parse the cue start and end times.
builder.setStartTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)))
.setEndTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(2)));
} catch (NumberFormatException e) {
Log.w(TAG, "Skipping cue with bad header: " + cueHeaderMatcher.group());
return false;
}
parseCueSettingsList(cueHeaderMatcher.group(3), builder);
// Parse the cue text.
textBuilder.setLength(0);
String line;
while ((line = webvttData.readLine()) != null && !line.isEmpty()) {
if (textBuilder.length() > 0) {
textBuilder.append("\n");
}
textBuilder.append(line.trim());
}
builder.setText(parseCueText(textBuilder.toString()));
return true;
}
/* package */ static Spanned parseCueText(String markup) {
/**
* Parses the text payload of a WebVTT Cue and applies modifications on {@link WebvttCue.Builder}.
*
* @param markup The markup text to be parsed.
* @param builder Target builder.
*/
/* package */ static void parseCueText(String markup, WebvttCue.Builder builder) {
SpannableStringBuilder spannedText = new SpannableStringBuilder();
Stack<StartTag> startTagStack = new Stack<>();
String[] tagTokens;
@ -231,7 +212,33 @@ public final class WebvttCueParser {
while (!startTagStack.isEmpty()) {
applySpansForTag(startTagStack.pop(), spannedText);
}
return spannedText;
builder.setText(spannedText);
}
private static boolean parseCue(Matcher cueHeaderMatcher, ParsableByteArray webvttData,
WebvttCue.Builder builder, StringBuilder textBuilder) {
try {
// Parse the cue start and end times.
builder.setStartTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)))
.setEndTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(2)));
} catch (NumberFormatException e) {
Log.w(TAG, "Skipping cue with bad header: " + cueHeaderMatcher.group());
return false;
}
parseCueSettingsList(cueHeaderMatcher.group(3), builder);
// Parse the cue text.
textBuilder.setLength(0);
String line;
while ((line = webvttData.readLine()) != null && !line.isEmpty()) {
if (textBuilder.length() > 0) {
textBuilder.append("\n");
}
textBuilder.append(line.trim());
}
parseCueText(textBuilder.toString(), builder);
return true;
}
// Internal methods