Commit b6b97a86 by aquilescanta Committed by Oliver Woodman

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
parent 6b9a1b16
......@@ -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;
}
}
}
......@@ -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,13 +117,45 @@ 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);
String line;
......@@ -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).
*/
......
......@@ -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);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment