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
This commit is contained in:
aquilescanta 2016-01-14 06:25:25 -08:00 committed by Oliver Woodman
parent 6b9a1b16f1
commit b6b97a8683
3 changed files with 200 additions and 125 deletions

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer.text.webvtt;
import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.text.Cue;
import android.text.Layout.Alignment; import android.text.Layout.Alignment;
import android.util.Log;
/** /**
* A representation of a WebVTT cue. * A representation of a WebVTT cue.
@ -53,4 +54,127 @@ import android.text.Layout.Alignment;
return (line == DIMEN_UNSET && position == DIMEN_UNSET); 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;
}
}
} }

View File

@ -31,7 +31,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; 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 { public final class WebvttCueParser {
@ -66,71 +66,50 @@ public final class WebvttCueParser {
private static final String TAG = "WebvttCueParser"; private static final String TAG = "WebvttCueParser";
private StringBuilder textBuilder; private final StringBuilder textBuilder;
private PositionHolder positionHolder;
public WebvttCueParser() { public WebvttCueParser() {
positionHolder = new PositionHolder();
textBuilder = new StringBuilder(); 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. * @param webvttData Parsable WebVTT file data.
* @return a {@link WebvttCue} instance if cue content is found. {@code null} otherwise. * @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; Matcher cueHeaderMatcher;
while ((cueHeaderMatcher = findNextCueHeader(webvttData)) != null) { while ((cueHeaderMatcher = findNextCueHeader(webvttData)) != null) {
WebvttCue currentCue = parseCue(cueHeaderMatcher, webvttData); if (parseCue(cueHeaderMatcher, webvttData, cueBuilder, textBuilder)) {
if (currentCue != null) { return true;
return currentCue;
} }
} }
return null; return false;
} }
private WebvttCue parseCue(Matcher cueHeaderMatcher, ParsableByteArray webvttData) { /**
long cueStartTime; * Parses a string containing a list of cue settings.
long cueEndTime; *
try { * @param cueSettingsList String containing the settings for a given cue.
// Parse the cue start and end times. * @param builder The {@link WebvttCue.Builder} where incremental construction takes place.
cueStartTime = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)); */
cueEndTime = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(2)); public static void parseCueSettingsList(String cueSettingsList, WebvttCue.Builder builder) {
} 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;
// Parse the cue settings list. // Parse the cue settings list.
Matcher cueSettingMatcher = CUE_SETTING_PATTERN.matcher(cueHeaderMatcher.group(3)); Matcher cueSettingMatcher = CUE_SETTING_PATTERN.matcher(cueSettingsList);
while (cueSettingMatcher.find()) { while (cueSettingMatcher.find()) {
String name = cueSettingMatcher.group(1); String name = cueSettingMatcher.group(1);
String value = cueSettingMatcher.group(2); String value = cueSettingMatcher.group(2);
try { try {
if ("line".equals(name)) { if ("line".equals(name)) {
parseLineAttribute(value, positionHolder); parseLineAttribute(value, builder);
cueLine = positionHolder.position;
cueLineType = positionHolder.lineType;
cueLineAnchor = positionHolder.positionAnchor;
} else if ("align".equals(name)) { } else if ("align".equals(name)) {
cueTextAlignment = parseTextAlignment(value); builder.setTextAlignment(parseTextAlignment(value));
} else if ("position".equals(name)) { } else if ("position".equals(name)) {
parsePositionAttribute(value, positionHolder); parsePositionAttribute(value, builder);
cuePosition = positionHolder.position;
cuePositionAnchor = positionHolder.positionAnchor;
} else if ("size".equals(name)) { } else if ("size".equals(name)) {
cueWidth = WebvttParserUtil.parsePercentage(value); builder.setWidth(WebvttParserUtil.parsePercentage(value));
} else { } else {
Log.w(TAG, "Unknown cue setting " + name + ":" + value); Log.w(TAG, "Unknown cue setting " + name + ":" + value);
} }
@ -138,13 +117,45 @@ public final class WebvttCueParser {
Log.w(TAG, "Skipping bad cue setting: " + cueSettingMatcher.group()); 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. // Parse the cue text.
textBuilder.setLength(0); textBuilder.setLength(0);
String line; String line;
@ -154,11 +165,8 @@ public final class WebvttCueParser {
} }
textBuilder.append(line.trim()); textBuilder.append(line.trim());
} }
builder.setText(parseCueText(textBuilder.toString()));
CharSequence cueText = parseCueText(textBuilder.toString()); return true;
return new WebvttCue(cueStartTime, cueEndTime, cueText, cueTextAlignment, cueLine,
cueLineType, cueLineAnchor, cuePosition, cuePositionAnchor, cueWidth);
} }
/* package */ static Spanned parseCueText(String markup) { /* package */ static Spanned parseCueText(String markup) {
@ -226,77 +234,34 @@ public final class WebvttCueParser {
return spannedText; 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 // Internal methods
private static void parseLineAttribute(String s, PositionHolder out) private static void parseLineAttribute(String s, WebvttCue.Builder builder)
throws NumberFormatException { throws NumberFormatException {
int lineAnchor;
int commaPosition = s.indexOf(','); int commaPosition = s.indexOf(',');
if (commaPosition != -1) { if (commaPosition != -1) {
lineAnchor = parsePositionAnchor(s.substring(commaPosition + 1)); builder.setLineAnchor(parsePositionAnchor(s.substring(commaPosition + 1)));
s = s.substring(0, commaPosition); s = s.substring(0, commaPosition);
} else { } else {
lineAnchor = Cue.TYPE_UNSET; builder.setLineAnchor(Cue.TYPE_UNSET);
} }
float line;
int lineType;
if (s.endsWith("%")) { if (s.endsWith("%")) {
line = WebvttParserUtil.parsePercentage(s); builder.setLine(WebvttParserUtil.parsePercentage(s)).setLineType(Cue.LINE_TYPE_FRACTION);
lineType = Cue.LINE_TYPE_FRACTION;
} else { } else {
line = Integer.parseInt(s); builder.setLine(Integer.parseInt(s)).setLineType(Cue.LINE_TYPE_NUMBER);
lineType = 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 { throws NumberFormatException {
int positionAnchor;
int commaPosition = s.indexOf(','); int commaPosition = s.indexOf(',');
if (commaPosition != -1) { if (commaPosition != -1) {
positionAnchor = parsePositionAnchor(s.substring(commaPosition + 1)); builder.setPositionAnchor(parsePositionAnchor(s.substring(commaPosition + 1)));
s = s.substring(0, commaPosition); s = s.substring(0, commaPosition);
} else { } else {
positionAnchor = Cue.TYPE_UNSET; builder.setPositionAnchor(Cue.TYPE_UNSET);
} }
out.position = WebvttParserUtil.parsePercentage(s); builder.setPosition(WebvttParserUtil.parsePercentage(s));
out.positionAnchor = positionAnchor;
out.lineType = Cue.TYPE_UNSET;
} }
private static int parsePositionAnchor(String 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). * 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. * @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). * @return the position of the end of tag plus 1 (one).
*/ */

View File

@ -33,10 +33,12 @@ public final class WebvttParser implements SubtitleParser {
private final WebvttCueParser cueParser; private final WebvttCueParser cueParser;
private final ParsableByteArray parsableWebvttData; private final ParsableByteArray parsableWebvttData;
private final WebvttCue.Builder webvttCueBuilder;
public WebvttParser() { public WebvttParser() {
cueParser = new WebvttCueParser(); cueParser = new WebvttCueParser();
parsableWebvttData = new ParsableByteArray(); parsableWebvttData = new ParsableByteArray();
webvttCueBuilder = new WebvttCue.Builder();
} }
@Override @Override
@ -48,6 +50,7 @@ public final class WebvttParser implements SubtitleParser {
public final WebvttSubtitle parse(byte[] bytes, int offset, int length) throws ParserException { public final WebvttSubtitle parse(byte[] bytes, int offset, int length) throws ParserException {
parsableWebvttData.reset(bytes, offset + length); parsableWebvttData.reset(bytes, offset + length);
parsableWebvttData.setPosition(offset); 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. // Validate the first line of the header, and skip the remainder.
WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData); WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData);
@ -55,9 +58,9 @@ public final class WebvttParser implements SubtitleParser {
// Extract Cues // Extract Cues
ArrayList<WebvttCue> subtitles = new ArrayList<>(); ArrayList<WebvttCue> subtitles = new ArrayList<>();
WebvttCue currentWebvttCue; while (cueParser.parseNextValidCue(parsableWebvttData, webvttCueBuilder)) {
while ((currentWebvttCue = cueParser.parseNextValidCue(parsableWebvttData)) != null) { subtitles.add(webvttCueBuilder.build());
subtitles.add(currentWebvttCue); webvttCueBuilder.reset();
} }
return new WebvttSubtitle(subtitles); return new WebvttSubtitle(subtitles);
} }