diff --git a/library/src/androidTest/java/com/google/android/exoplayer/text/mp4webvtt/Mp4WebvttParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer/text/webvtt/Mp4WebvttParserTest.java similarity index 88% rename from library/src/androidTest/java/com/google/android/exoplayer/text/mp4webvtt/Mp4WebvttParserTest.java rename to library/src/androidTest/java/com/google/android/exoplayer/text/webvtt/Mp4WebvttParserTest.java index 05f24de511..2444dcca6f 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/text/mp4webvtt/Mp4WebvttParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/text/webvtt/Mp4WebvttParserTest.java @@ -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); diff --git a/library/src/androidTest/java/com/google/android/exoplayer/text/webvtt/WebvttCueParserTest.java b/library/src/androidTest/java/com/google/android/exoplayer/text/webvtt/WebvttCueParserTest.java index f5a7af9790..0737476ad9 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/text/webvtt/WebvttCueParserTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/text/webvtt/WebvttCueParserTest.java @@ -27,7 +27,7 @@ import android.text.style.UnderlineSpan; public final class WebvttCueParserTest extends InstrumentationTestCase { public void testParseStrictValidClassesAndTrailingTokens() throws Exception { - Spanned text = WebvttCueParser.parseCueText("" + Spanned text = parseCueText("" + "This is text with html 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( - "This is text with " + Spanned text = parseCueText("This is text with " + "html 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 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()); @@ -76,8 +74,7 @@ public final class WebvttCueParserTest extends InstrumentationTestCase { } public void testParseWellFormedUnclosedEndAtParent() throws Exception { - Spanned text = WebvttCueParser.parseCueText( - "An unclosed u tag with underline and italic inside"); + Spanned text = parseCueText("An unclosed u tag with underline and italic 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( - "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()); 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("blahblahblahblah"); + Spanned text = parseCueText("blahblahblahblah"); 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("& > <  "); + Spanned text = parseCueText("& > <  "); 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("& here comes text"); + Spanned text = parseCueText("& 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
text
"); + Spanned text = parseCueText("here comes
text
"); assertEquals("here comes text", text.toString()); } public void testParseMultipleTagsOfSameKind() { - Spanned text = WebvttCueParser.parseCueText("blah blah blah foo"); + Spanned text = parseCueText("blah blah blah foo"); 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 blah"); + Spanned text = parseCueText("blah 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
inside"); + Spanned text = parseCueText("< u>An unclosed u tag with <<<<< i>italic" + + " inside"); assertEquals("An unclosed u tag with italic inside", text.toString()); - text = WebvttCueParser.parseCueText(">>>>>>>>>An unclosed u tag with <<<<< italic" + text = parseCueText(">>>>>>>>>An unclosed u tag with <<<<< italic" + " 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("><<<<<<<<<<"); + 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[] getSpans(Spanned text, Class spanType) { return text.getSpans(0, text.length(), spanType); } diff --git a/library/src/main/java/com/google/android/exoplayer/text/mp4webvtt/Mp4WebvttParser.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/Mp4WebvttParser.java similarity index 73% rename from library/src/main/java/com/google/android/exoplayer/text/mp4webvtt/Mp4WebvttParser.java rename to library/src/main/java/com/google/android/exoplayer/text/webvtt/Mp4WebvttParser.java index 3c165b90fa..41533d9df6 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/mp4webvtt/Mp4WebvttParser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/Mp4WebvttParser.java @@ -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(); } } diff --git a/library/src/main/java/com/google/android/exoplayer/text/mp4webvtt/Mp4WebvttSubtitle.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/Mp4WebvttSubtitle.java similarity index 96% rename from library/src/main/java/com/google/android/exoplayer/text/mp4webvtt/Mp4WebvttSubtitle.java rename to library/src/main/java/com/google/android/exoplayer/text/webvtt/Mp4WebvttSubtitle.java index bbdcc517ec..474abafbc1 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/mp4webvtt/Mp4WebvttSubtitle.java +++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/Mp4WebvttSubtitle.java @@ -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; 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 d4b77a0e74..7c65a266d7 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 @@ -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 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