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 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;
}
}
}

View File

@ -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).
*/

View File

@ -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<WebvttCue> 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);
}