From b6b97a8683864ce39d7bfca9f817556fcddcbe4d Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 14 Jan 2016 06:25:25 -0800 Subject: [PATCH] Expose cue settings parser This CL exposes the cue settings parser in order to allow its usage from the MP4Webvtt extractor. Also fixes a few mistakes from the previous related CL. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=112145806 --- .../exoplayer/text/webvtt/WebvttCue.java | 124 +++++++++++ .../text/webvtt/WebvttCueParser.java | 192 +++++++----------- .../exoplayer/text/webvtt/WebvttParser.java | 9 +- 3 files changed, 200 insertions(+), 125 deletions(-) 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 3b0d233007..95113cfb34 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,6 +18,7 @@ package com.google.android.exoplayer.text.webvtt; import com.google.android.exoplayer.text.Cue; import android.text.Layout.Alignment; +import android.util.Log; /** * A representation of a WebVTT cue. @@ -53,4 +54,127 @@ import android.text.Layout.Alignment; return (line == DIMEN_UNSET && position == DIMEN_UNSET); } + /** + * Builder for WebVTT cues. + */ + @SuppressWarnings("hiding") + public static final class Builder { + + private static final String TAG = "WebvttCueBuilder"; + + private long startTime; + private long endTime; + private CharSequence text; + private Alignment textAlignment; + private float line; + private int lineType; + private int lineAnchor; + private float position; + private int positionAnchor; + private float width; + + // Initialization methods + + public Builder() { + reset(); + } + + public void reset() { + startTime = 0; + endTime = 0; + text = null; + textAlignment = null; + line = Cue.DIMEN_UNSET; + lineType = Cue.TYPE_UNSET; + lineAnchor = Cue.TYPE_UNSET; + position = Cue.DIMEN_UNSET; + positionAnchor = Cue.TYPE_UNSET; + width = Cue.DIMEN_UNSET; + } + + // Construction methods + + public WebvttCue build() { + if (position != Cue.DIMEN_UNSET && positionAnchor == Cue.TYPE_UNSET) { + derivePositionAnchorFromAlignment(); + } + return new WebvttCue(startTime, endTime, text, textAlignment, line, lineType, lineAnchor, + position, positionAnchor, width); + } + + public Builder setStartTime(long time) { + startTime = time; + return this; + } + + public Builder setEndTime(long time) { + endTime = time; + return this; + } + + public Builder setText(CharSequence aText) { + text = aText; + return this; + } + + public Builder setTextAlignment(Alignment textAlignment) { + this.textAlignment = textAlignment; + return this; + } + + public Builder setLine(float line) { + this.line = line; + return this; + } + + public Builder setLineType(int lineType) { + this.lineType = lineType; + return this; + } + + public Builder setLineAnchor(int lineAnchor) { + this.lineAnchor = lineAnchor; + return this; + } + + public Builder setPosition(float position) { + this.position = position; + return this; + } + + public Builder setPositionAnchor(int positionAnchor) { + this.positionAnchor = positionAnchor; + return this; + } + + public Builder setWidth(float width) { + this.width = width; + return this; + } + + private Builder derivePositionAnchorFromAlignment() { + if (textAlignment == null) { + positionAnchor = Cue.TYPE_UNSET; + } else { + switch (textAlignment) { + case ALIGN_NORMAL: + positionAnchor = Cue.ANCHOR_TYPE_START; + break; + case ALIGN_CENTER: + positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; + break; + case ALIGN_OPPOSITE: + positionAnchor = Cue.ANCHOR_TYPE_END; + break; + default: + Log.w(TAG, "Unrecognized alignment: " + textAlignment); + positionAnchor = Cue.ANCHOR_TYPE_START; + break; + } + } + return this; + } + + } + } 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 e0322a3d85..d4b77a0e74 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 @@ -31,7 +31,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * Parser for webvtt cue text. (https://w3c.github.io/webvtt/#cue-text) + * Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) */ public final class WebvttCueParser { @@ -66,71 +66,50 @@ public final class WebvttCueParser { private static final String TAG = "WebvttCueParser"; - private StringBuilder textBuilder; - private PositionHolder positionHolder; + private final StringBuilder textBuilder; public WebvttCueParser() { - positionHolder = new PositionHolder(); textBuilder = new StringBuilder(); } /** - * Parses the next valid Webvtt cue in a parsable array, including timestamps, settings and text. + * Parses the next valid WebVTT cue in a parsable array, including timestamps, settings and text. * - * @param webvttData parsable Webvtt file data. - * @return a {@link WebvttCue} instance if cue content is found. {@code null} otherwise. + * @param webvttData Parsable WebVTT file data. + * @param cueBuilder Builder for WebVTT Cues. + * @return True if a valid Cue was found, false otherwise. */ - public WebvttCue parseNextValidCue(ParsableByteArray webvttData) { + public boolean parseNextValidCue(ParsableByteArray webvttData, WebvttCue.Builder cueBuilder) { Matcher cueHeaderMatcher; while ((cueHeaderMatcher = findNextCueHeader(webvttData)) != null) { - WebvttCue currentCue = parseCue(cueHeaderMatcher, webvttData); - if (currentCue != null) { - return currentCue; + if (parseCue(cueHeaderMatcher, webvttData, cueBuilder, textBuilder)) { + return true; } } - return null; + return false; } - private WebvttCue parseCue(Matcher cueHeaderMatcher, ParsableByteArray webvttData) { - long cueStartTime; - long cueEndTime; - try { - // Parse the cue start and end times. - cueStartTime = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)); - cueEndTime = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(2)); - } catch (NumberFormatException e) { - Log.w(TAG, "Skipping cue with bad header: " + cueHeaderMatcher.group()); - return null; - } - - // Default cue settings. - Alignment cueTextAlignment = null; - float cueLine = Cue.DIMEN_UNSET; - int cueLineType = Cue.TYPE_UNSET; - int cueLineAnchor = Cue.TYPE_UNSET; - float cuePosition = Cue.DIMEN_UNSET; - int cuePositionAnchor = Cue.TYPE_UNSET; - float cueWidth = Cue.DIMEN_UNSET; - + /** + * Parses a string containing a list of cue settings. + * + * @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) { // Parse the cue settings list. - Matcher cueSettingMatcher = CUE_SETTING_PATTERN.matcher(cueHeaderMatcher.group(3)); + Matcher cueSettingMatcher = CUE_SETTING_PATTERN.matcher(cueSettingsList); while (cueSettingMatcher.find()) { String name = cueSettingMatcher.group(1); String value = cueSettingMatcher.group(2); try { if ("line".equals(name)) { - parseLineAttribute(value, positionHolder); - cueLine = positionHolder.position; - cueLineType = positionHolder.lineType; - cueLineAnchor = positionHolder.positionAnchor; + parseLineAttribute(value, builder); } else if ("align".equals(name)) { - cueTextAlignment = parseTextAlignment(value); + builder.setTextAlignment(parseTextAlignment(value)); } else if ("position".equals(name)) { - parsePositionAttribute(value, positionHolder); - cuePosition = positionHolder.position; - cuePositionAnchor = positionHolder.positionAnchor; + parsePositionAttribute(value, builder); } else if ("size".equals(name)) { - cueWidth = WebvttParserUtil.parsePercentage(value); + builder.setWidth(WebvttParserUtil.parsePercentage(value)); } else { Log.w(TAG, "Unknown cue setting " + name + ":" + value); } @@ -138,12 +117,44 @@ public final class WebvttCueParser { Log.w(TAG, "Skipping bad cue setting: " + cueSettingMatcher.group()); } } + } - if (cuePosition != Cue.DIMEN_UNSET && cuePositionAnchor == Cue.TYPE_UNSET) { - // Computed position alignment should be derived from the text alignment if it has not been - // set explicitly. - cuePositionAnchor = alignmentToAnchor(cueTextAlignment); + /** + * 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; + } + + 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); @@ -154,11 +165,8 @@ public final class WebvttCueParser { } textBuilder.append(line.trim()); } - - CharSequence cueText = parseCueText(textBuilder.toString()); - - return new WebvttCue(cueStartTime, cueEndTime, cueText, cueTextAlignment, cueLine, - cueLineType, cueLineAnchor, cuePosition, cuePositionAnchor, cueWidth); + builder.setText(parseCueText(textBuilder.toString())); + return true; } /* package */ static Spanned parseCueText(String markup) { @@ -226,77 +234,34 @@ public final class WebvttCueParser { return spannedText; } - /** - * 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; - } - - private static final class PositionHolder { - - public float position; - public int positionAnchor; - public int lineType; - - } - // Internal methods - private static void parseLineAttribute(String s, PositionHolder out) + private static void parseLineAttribute(String s, WebvttCue.Builder builder) throws NumberFormatException { - int lineAnchor; int commaPosition = s.indexOf(','); if (commaPosition != -1) { - lineAnchor = parsePositionAnchor(s.substring(commaPosition + 1)); + builder.setLineAnchor(parsePositionAnchor(s.substring(commaPosition + 1))); s = s.substring(0, commaPosition); } else { - lineAnchor = Cue.TYPE_UNSET; + builder.setLineAnchor(Cue.TYPE_UNSET); } - float line; - int lineType; if (s.endsWith("%")) { - line = WebvttParserUtil.parsePercentage(s); - lineType = Cue.LINE_TYPE_FRACTION; + builder.setLine(WebvttParserUtil.parsePercentage(s)).setLineType(Cue.LINE_TYPE_FRACTION); } else { - line = Integer.parseInt(s); - lineType = Cue.LINE_TYPE_NUMBER; + builder.setLine(Integer.parseInt(s)).setLineType(Cue.LINE_TYPE_NUMBER); } - out.position = line; - out.positionAnchor = lineAnchor; - out.lineType = lineType; } - private static void parsePositionAttribute(String s, PositionHolder out) + private static void parsePositionAttribute(String s, WebvttCue.Builder builder) throws NumberFormatException { - int positionAnchor; int commaPosition = s.indexOf(','); if (commaPosition != -1) { - positionAnchor = parsePositionAnchor(s.substring(commaPosition + 1)); + builder.setPositionAnchor(parsePositionAnchor(s.substring(commaPosition + 1))); s = s.substring(0, commaPosition); } else { - positionAnchor = Cue.TYPE_UNSET; + builder.setPositionAnchor(Cue.TYPE_UNSET); } - out.position = WebvttParserUtil.parsePercentage(s); - out.positionAnchor = positionAnchor; - out.lineType = Cue.TYPE_UNSET; + builder.setPosition(WebvttParserUtil.parsePercentage(s)); } private static int parsePositionAnchor(String s) { @@ -329,27 +294,10 @@ public final class WebvttCueParser { } } - private static int alignmentToAnchor(Alignment alignment) { - if (alignment == null) { - return Cue.TYPE_UNSET; - } - switch (alignment) { - case ALIGN_NORMAL: - return Cue.ANCHOR_TYPE_START; - case ALIGN_CENTER: - return Cue.ANCHOR_TYPE_MIDDLE; - case ALIGN_OPPOSITE: - return Cue.ANCHOR_TYPE_END; - default: - Log.w(TAG, "Unrecognized alignment: " + alignment); - return Cue.ANCHOR_TYPE_START; - } - } - /** * 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 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). */ 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 90ec748fc1..3484259724 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 @@ -33,10 +33,12 @@ public final class WebvttParser implements SubtitleParser { private final WebvttCueParser cueParser; private final ParsableByteArray parsableWebvttData; + private final WebvttCue.Builder webvttCueBuilder; public WebvttParser() { cueParser = new WebvttCueParser(); parsableWebvttData = new ParsableByteArray(); + webvttCueBuilder = new WebvttCue.Builder(); } @Override @@ -48,6 +50,7 @@ public final class WebvttParser implements SubtitleParser { public final WebvttSubtitle parse(byte[] bytes, int offset, int length) throws ParserException { parsableWebvttData.reset(bytes, offset + length); parsableWebvttData.setPosition(offset); + webvttCueBuilder.reset(); // In case a previous parse run failed with a ParserException. // Validate the first line of the header, and skip the remainder. WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData); @@ -55,9 +58,9 @@ public final class WebvttParser implements SubtitleParser { // Extract Cues ArrayList subtitles = new ArrayList<>(); - WebvttCue currentWebvttCue; - while ((currentWebvttCue = cueParser.parseNextValidCue(parsableWebvttData)) != null) { - subtitles.add(currentWebvttCue); + while (cueParser.parseNextValidCue(parsableWebvttData, webvttCueBuilder)) { + subtitles.add(webvttCueBuilder.build()); + webvttCueBuilder.reset(); } return new WebvttSubtitle(subtitles); }