diff --git a/library/src/androidTest/assets/webvtt/with_css_styles b/library/src/androidTest/assets/webvtt/with_css_styles new file mode 100644 index 0000000000..ec7caace6d --- /dev/null +++ b/library/src/androidTest/assets/webvtt/with_css_styles @@ -0,0 +1,23 @@ +WEBVTT + +STYLE +::cue { + background-color: green; + color: papayawhip; +} +/* Style blocks cannot use blank lines nor "dash dash greater than" */ + +NOTE comment blocks can be used between style blocks. + +STYLE +::cue(2) { + color: peachpuff; +} + +1 +00:00.000 --> 00:01.234 +This is the first subtitle. + +2 +00:02.345 --> 00:03.456 +This is the second subtitle. 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 9c2ffe78ae..0d6152bdc5 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 @@ -21,6 +21,9 @@ import com.google.android.exoplayer.text.Cue; import android.test.InstrumentationTestCase; import android.text.Layout.Alignment; +import android.text.Spanned; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; import java.io.IOException; import java.util.List; @@ -36,6 +39,7 @@ public class WebvttParserTest extends InstrumentationTestCase { private static final String WITH_POSITIONING_FILE = "webvtt/with_positioning"; private static final String WITH_BAD_CUE_HEADER_FILE = "webvtt/with_bad_cue_header"; private static final String WITH_TAGS_FILE = "webvtt/with_tags"; + private static final String WITH_CSS_STYLES = "webvtt/with_css_styles"; private static final String EMPTY_FILE = "webvtt/empty"; public void testParseEmpty() throws IOException { @@ -54,10 +58,10 @@ public class WebvttParserTest extends InstrumentationTestCase { byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_FILE); WebvttSubtitle subtitle = parser.decode(bytes, bytes.length); - // test event count + // Test event count. assertEquals(4, subtitle.getEventTimeCount()); - // test cues + // Test cues. assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle."); assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle."); } @@ -67,10 +71,10 @@ public class WebvttParserTest extends InstrumentationTestCase { byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_WITH_IDS_FILE); WebvttSubtitle subtitle = parser.decode(bytes, bytes.length); - // test event count + // Test event count. assertEquals(4, subtitle.getEventTimeCount()); - // test cues + // Test cues. assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle."); assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle."); } @@ -93,10 +97,10 @@ public class WebvttParserTest extends InstrumentationTestCase { byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_TAGS_FILE); WebvttSubtitle subtitle = parser.decode(bytes, bytes.length); - // test event count + // Test event count. assertEquals(8, subtitle.getEventTimeCount()); - // test cues + // Test cues. assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle."); assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle."); assertCue(subtitle, 4, 4000000, 5000000, "This is the third subtitle."); @@ -108,10 +112,10 @@ public class WebvttParserTest extends InstrumentationTestCase { byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_POSITIONING_FILE); WebvttSubtitle subtitle = parser.decode(bytes, bytes.length); - // test event count + // Test event count. assertEquals(12, subtitle.getEventTimeCount()); - // test cues + // Test cues. assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.", Alignment.ALIGN_NORMAL, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, 0.1f, Cue.ANCHOR_TYPE_START, 0.35f); assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.", @@ -136,13 +140,34 @@ public class WebvttParserTest extends InstrumentationTestCase { byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_BAD_CUE_HEADER_FILE); WebvttSubtitle subtitle = parser.decode(bytes, bytes.length); - // test event count + // Test event count. assertEquals(4, subtitle.getEventTimeCount()); - // test cues + // Test cues. assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle."); assertCue(subtitle, 2, 4000000, 5000000, "This is the third subtitle."); } + + public void testWebvttWithCssStyle() throws IOException { + WebvttParser parser = new WebvttParser(); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), WITH_CSS_STYLES); + WebvttSubtitle subtitle = parser.decode(bytes, bytes.length); + + // Test event count. + assertEquals(4, subtitle.getEventTimeCount()); + + // Test cues. + assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle."); + assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle."); + + Cue cue1 = subtitle.getCues(0).get(0); + Cue cue2 = subtitle.getCues(2345000).get(0); + Spanned s1 = (Spanned) cue1.text; + Spanned s2 = (Spanned) cue2.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); + } private static void assertCue(WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs, int endTimeUs, String text) { @@ -157,7 +182,7 @@ public class WebvttParserTest extends InstrumentationTestCase { assertEquals(endTimeUs, subtitle.getEventTime(eventTimeIndex + 1)); List cues = subtitle.getCues(subtitle.getEventTime(eventTimeIndex)); assertEquals(1, cues.size()); - // Assert cue properties + // Assert cue properties. Cue cue = cues.get(0); assertEquals(text, cue.text.toString()); assertEquals(textAlignment, cue.textAlignment); diff --git a/library/src/main/java/com/google/android/exoplayer/hls/WebvttExtractor.java b/library/src/main/java/com/google/android/exoplayer/hls/WebvttExtractor.java index 58ab520dd6..418390a06d 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/WebvttExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/WebvttExtractor.java @@ -25,7 +25,6 @@ import com.google.android.exoplayer.extractor.PositionHolder; import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.extractor.TrackOutput; import com.google.android.exoplayer.extractor.ts.PtsTimestampAdjuster; -import com.google.android.exoplayer.text.webvtt.WebvttCueParser; import com.google.android.exoplayer.text.webvtt.WebvttParserUtil; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.ParsableByteArray; @@ -145,7 +144,7 @@ import java.util.regex.Pattern; } // Find the first cue header and parse the start time. - Matcher cueHeaderMatcher = WebvttCueParser.findNextCueHeader(webvttData); + Matcher cueHeaderMatcher = WebvttParserUtil.findNextCueHeader(webvttData); if (cueHeaderMatcher == null) { // No cues found. Don't output a sample, but still output a corresponding track. buildTrackOutput(0); 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 1a8751a510..8573d72dd8 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 @@ -54,6 +54,7 @@ import java.util.Map; * @param styleMap The map that contains styles accessible by selector. */ public void parseBlock(ParsableByteArray input, Map styleMap) { + stringBuilder.setLength(0); int initialInputPosition = input.getPosition(); skipStyleBlock(input); styleInput.reset(input.data, input.getPosition()); @@ -97,8 +98,8 @@ import java.util.Map; * ::cue(v[voice="Someone"]) * * @param input From which the selector is obtained. - * @return A string containing the target, empty string if targets all cues and null if an error - * was encountered. + * @return A string containing the target, {@link WebvttCue#UNIVERSAL_CUE_ID} if targets all cues + * and null if an error was encountered. */ private static String parseSelector(ParsableByteArray input, StringBuilder stringBuilder) { skipWhitespaceAndComments(input); @@ -116,7 +117,7 @@ import java.util.Map; } if ("{".equals(token)) { input.setPosition(position); - return ""; + return WebvttCue.UNIVERSAL_CUE_ID; } String target = null; if ("(".equals(token)) { 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 95113cfb34..f2ea42fded 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,13 +18,31 @@ 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 final String id; public final long startTime; public final long endTime; @@ -33,13 +51,15 @@ import android.util.Log; } public WebvttCue(long startTime, long endTime, CharSequence text) { - this(startTime, endTime, text, null, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, + this(null, startTime, endTime, text, null, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); } - public WebvttCue(long startTime, long endTime, CharSequence text, Alignment textAlignment, - float line, int lineType, int lineAnchor, float position, int positionAnchor, float width) { + public WebvttCue(String id, 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; } @@ -62,9 +82,10 @@ import android.util.Log; private static final String TAG = "WebvttCueBuilder"; + private String id; private long startTime; private long endTime; - private CharSequence text; + private SpannableStringBuilder text; private Alignment textAlignment; private float line; private int lineType; @@ -92,16 +113,28 @@ import android.util.Log; width = Cue.DIMEN_UNSET; } - // Construction methods + // 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(id), 0, text.length()); if (position != Cue.DIMEN_UNSET && positionAnchor == Cue.TYPE_UNSET) { derivePositionAnchorFromAlignment(); } - return new WebvttCue(startTime, endTime, text, textAlignment, line, lineType, lineAnchor, + return new WebvttCue(id, 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; @@ -112,7 +145,7 @@ import android.util.Log; return this; } - public Builder setText(CharSequence aText) { + public Builder setText(SpannableStringBuilder aText) { text = aText; return this; } @@ -175,6 +208,54 @@ import android.util.Log; 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 e681dd32e4..2635a2ee8c 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 @@ -33,12 +33,11 @@ import java.util.regex.Pattern; /** * Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) */ -public final class WebvttCueParser { +/* package */ final class WebvttCueParser { public static final Pattern CUE_HEADER_PATTERN = Pattern .compile("^(\\S+)\\s+-->\\s+(\\S+)(.*)?$"); - private static final Pattern COMMENT = Pattern.compile("^NOTE((\u0020|\u0009).*)?$"); private static final Pattern CUE_SETTING_PATTERN = Pattern.compile("(\\S+?):(\\S+)"); private static final char CHAR_LESS_THAN = '<'; @@ -71,7 +70,7 @@ public final class WebvttCueParser { public WebvttCueParser() { textBuilder = new StringBuilder(); } - + /** * Parses the next valid WebVTT cue in a parsable array, including timestamps, settings and text. * @@ -79,11 +78,20 @@ public final class WebvttCueParser { * @param builder Builder for WebVTT Cues. * @return True if a valid Cue was found, false otherwise. */ - /* package */ boolean parseNextValidCue(ParsableByteArray webvttData, WebvttCue.Builder builder) { - Matcher cueHeaderMatcher; - while ((cueHeaderMatcher = findNextCueHeader(webvttData)) != null) { - if (parseCue(cueHeaderMatcher, webvttData, builder, textBuilder)) { - return true; + /* package */ boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder) { + 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); + } 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 false; @@ -120,30 +128,6 @@ public final class WebvttCueParser { } } - /** - * Reads lines up to and including the next WebVTT cue header. - * - * @param input The input from which lines should be read. - * @return A {@link Matcher} for the WebVTT cue header, or null if the end of the input was - * reached without a cue header being found. In the case that a cue header is found, groups 1, - * 2 and 3 of the returned matcher contain the start time, end time and settings list. - */ - public static Matcher findNextCueHeader(ParsableByteArray input) { - String line; - while ((line = input.readLine()) != null) { - if (COMMENT.matcher(line).matches()) { - // Skip until the end of the comment block. - while ((line = input.readLine()) != null && !line.isEmpty()) {} - } else { - Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(line); - if (cueHeaderMatcher.matches()) { - return cueHeaderMatcher; - } - } - } - return null; - } - /** * Parses the text payload of a WebVTT Cue and applies modifications on {@link WebvttCue.Builder}. * 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 cad5060cc4..d7b5c9fbbc 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 @@ -22,6 +22,7 @@ import com.google.android.exoplayer.util.ParsableByteArray; import android.text.TextUtils; import java.util.ArrayList; +import java.util.HashMap; /** * A simple WebVTT parser. @@ -30,32 +31,88 @@ import java.util.ArrayList; */ public final class WebvttParser extends SubtitleParser { + private static final int NO_EVENT_FOUND = -1; + private static final int END_OF_FILE_FOUND = 0; + private static final int COMMENT_FOUND = 1; + private static final int STYLE_BLOCK_FOUND = 2; + private static final int CUE_FOUND = 3; + private static final String COMMENT_START = "NOTE"; + private static final String STYLE_START = "STYLE"; + private final WebvttCueParser cueParser; private final ParsableByteArray parsableWebvttData; private final WebvttCue.Builder webvttCueBuilder; + private final CssParser cssParser; + private final HashMap styleMap; public WebvttParser() { cueParser = new WebvttCueParser(); parsableWebvttData = new ParsableByteArray(); webvttCueBuilder = new WebvttCue.Builder(); + cssParser = new CssParser(); + styleMap = new HashMap<>(); } @Override - protected final WebvttSubtitle decode(byte[] bytes, int length) throws ParserException { + protected WebvttSubtitle decode(byte[] bytes, int length) throws ParserException { parsableWebvttData.reset(bytes, length); - webvttCueBuilder.reset(); // In case a previous parse run failed with a ParserException. + // Initialization for consistent starting state. + webvttCueBuilder.reset(); + styleMap.clear(); // Validate the first line of the header, and skip the remainder. WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData); while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {} - - // Extract Cues + + int eventFound; ArrayList subtitles = new ArrayList<>(); - while (cueParser.parseNextValidCue(parsableWebvttData, webvttCueBuilder)) { - subtitles.add(webvttCueBuilder.build()); - webvttCueBuilder.reset(); + while ((eventFound = getNextEvent(parsableWebvttData)) != END_OF_FILE_FOUND) { + if (eventFound == COMMENT_FOUND) { + skipComment(parsableWebvttData); + } else if (eventFound == STYLE_BLOCK_FOUND) { + if (!subtitles.isEmpty()) { + throw new ParserException("A style block was found after the first cue."); + } + 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)); + webvttCueBuilder.reset(); + } + } } return new WebvttSubtitle(subtitles); } + + /** + * Positions the input right before the next event, and returns the kind of event found. Does not + * consume any data from such event, if any. + * + * @return The kind of event found. + */ + private static int getNextEvent(ParsableByteArray parsableWebvttData) { + int foundEvent = NO_EVENT_FOUND; + int currentInputPosition = 0; + while (foundEvent == NO_EVENT_FOUND) { + currentInputPosition = parsableWebvttData.getPosition(); + String line = parsableWebvttData.readLine(); + if (line == null) { + foundEvent = END_OF_FILE_FOUND; + } else if (STYLE_START.equals(line)) { + foundEvent = STYLE_BLOCK_FOUND; + } else if (COMMENT_START.startsWith(line)) { + foundEvent = COMMENT_FOUND; + } else { + foundEvent = CUE_FOUND; + } + } + parsableWebvttData.setPosition(currentInputPosition); + return foundEvent; + } + + private static void skipComment(ParsableByteArray parsableWebvttData) { + while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {} + } } diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParserUtil.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParserUtil.java index 9f1d6a4f36..dec1a1823c 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParserUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttParserUtil.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer.text.webvtt; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.util.ParsableByteArray; +import java.util.regex.Matcher; import java.util.regex.Pattern; /** @@ -25,6 +26,7 @@ import java.util.regex.Pattern; */ public final class WebvttParserUtil { + private static final Pattern COMMENT = Pattern.compile("^NOTE((\u0020|\u0009).*)?$"); private static final Pattern HEADER = Pattern.compile("^\uFEFF?WEBVTT((\u0020|\u0009).*)?$"); private WebvttParserUtil() {} @@ -71,5 +73,29 @@ public final class WebvttParserUtil { } return Float.parseFloat(s.substring(0, s.length() - 1)) / 100; } + + /** + * Reads lines up to and including the next WebVTT cue header. + * + * @param input The input from which lines should be read. + * @return A {@link Matcher} for the WebVTT cue header, or null if the end of the input was + * reached without a cue header being found. In the case that a cue header is found, groups 1, + * 2 and 3 of the returned matcher contain the start time, end time and settings list. + */ + public static Matcher findNextCueHeader(ParsableByteArray input) { + String line; + while ((line = input.readLine()) != null) { + if (COMMENT.matcher(line).matches()) { + // Skip until the end of the comment block. + while ((line = input.readLine()) != null && !line.isEmpty()) {} + } else { + Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(line); + if (cueHeaderMatcher.matches()) { + return cueHeaderMatcher; + } + } + } + return null; + } }