Commit 8cf4042d by ibaker Committed by Oliver Woodman

Move WebvttCueInfo.Builder inside WebvttCueParser

This class is only used to hold temporary data while we parse the
settings and text, so we don't need it outside the Parser class.

Also remove all state from WebvttCueParser - this increases
the number of allocations, but there are already many
and  subtitles generally aren't very frequent (compared to
e.g. video frames).

PiperOrigin-RevId: 286200002
parent a8d39c11
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.text.webvtt;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
import com.google.android.exoplayer2.text.Subtitle;
......@@ -41,12 +42,10 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder {
private static final int TYPE_vttc = 0x76747463;
private final ParsableByteArray sampleData;
private final WebvttCueInfo.Builder builder;
public Mp4WebvttDecoder() {
super("Mp4WebvttDecoder");
sampleData = new ParsableByteArray();
builder = new WebvttCueInfo.Builder();
}
@Override
......@@ -63,7 +62,7 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder {
int boxSize = sampleData.readInt();
int boxType = sampleData.readInt();
if (boxType == TYPE_vttc) {
resultingCueList.add(parseVttCueBox(sampleData, builder, boxSize - BOX_HEADER_SIZE));
resultingCueList.add(parseVttCueBox(sampleData, boxSize - BOX_HEADER_SIZE));
} else {
// Peers of the VTTCueBox are still not supported and are skipped.
sampleData.skipBytes(boxSize - BOX_HEADER_SIZE);
......@@ -72,10 +71,10 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder {
return new Mp4WebvttSubtitle(resultingCueList);
}
private static Cue parseVttCueBox(
ParsableByteArray sampleData, WebvttCueInfo.Builder builder, int remainingCueBoxBytes)
private static Cue parseVttCueBox(ParsableByteArray sampleData, int remainingCueBoxBytes)
throws SubtitleDecoderException {
builder.reset();
@Nullable Cue.Builder cueBuilder = null;
@Nullable CharSequence cueText = null;
while (remainingCueBoxBytes > 0) {
if (remainingCueBoxBytes < BOX_HEADER_SIZE) {
throw new SubtitleDecoderException("Incomplete vtt cue box header found.");
......@@ -89,14 +88,20 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder {
sampleData.skipBytes(payloadLength);
remainingCueBoxBytes -= payloadLength;
if (boxType == TYPE_sttg) {
WebvttCueParser.parseCueSettingsList(boxPayload, builder);
cueBuilder = WebvttCueParser.parseCueSettingsList(boxPayload);
} else if (boxType == TYPE_payl) {
WebvttCueParser.parseCueText(null, boxPayload.trim(), builder, Collections.emptyList());
cueText =
WebvttCueParser.parseCueText(
/* id= */ null, boxPayload.trim(), /* styles= */ Collections.emptyList());
} else {
// Other VTTCueBox children are still not supported and are ignored.
}
}
return builder.build().cue;
if (cueText == null) {
cueText = "";
}
return cueBuilder != null
? cueBuilder.setText(cueText).build()
: WebvttCueParser.newCueForText(cueText);
}
}
......@@ -15,296 +15,19 @@
*/
package com.google.android.exoplayer2.text.webvtt;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.text.Layout.Alignment;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
/** A representation of a WebVTT cue. */
public final class WebvttCueInfo {
/* package */ static final float DEFAULT_POSITION = 0.5f;
public final Cue cue;
public final long startTime;
public final long endTime;
private WebvttCueInfo(
long startTime,
long endTime,
CharSequence text,
@Nullable Alignment textAlignment,
float line,
@Cue.LineType int lineType,
@Cue.AnchorType int lineAnchor,
float position,
@Cue.AnchorType int positionAnchor,
float width) {
this.cue =
new Cue(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width);
this.startTime = startTime;
this.endTime = endTime;
}
/** Builder for WebVTT cues. */
@SuppressWarnings("hiding")
public static class Builder {
/**
* Valid values for {@link #setTextAlignment(int)}.
*
* <p>We use a custom list (and not {@link Alignment} directly) in order to include both {@code
* START}/{@code LEFT} and {@code END}/{@code RIGHT}. The distinction is important for {@link
* #derivePosition(int)}.
*
* <p>These correspond to the valid values for the 'align' cue setting in the <a
* href="https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment">WebVTT spec</a>.
*/
@Documented
@Retention(SOURCE)
@IntDef({
TextAlignment.START,
TextAlignment.CENTER,
TextAlignment.END,
TextAlignment.LEFT,
TextAlignment.RIGHT
})
public @interface TextAlignment {
/**
* See WebVTT's <a
* href="https://www.w3.org/TR/webvtt1/#webvtt-cue-start-alignment">align:start</a>.
*/
int START = 1;
/**
* See WebVTT's <a
* href="https://www.w3.org/TR/webvtt1/#webvtt-cue-center-alignment">align:center</a>.
*/
int CENTER = 2;
/**
* See WebVTT's <a
* href="https://www.w3.org/TR/webvtt1/#webvtt-cue-end-alignment">align:end</a>.
*/
int END = 3;
/**
* See WebVTT's <a
* href="https://www.w3.org/TR/webvtt1/#webvtt-cue-left-alignment">align:left</a>.
*/
int LEFT = 4;
/**
* See WebVTT's <a
* href="https://www.w3.org/TR/webvtt1/#webvtt-cue-right-alignment">align:right</a>.
*/
int RIGHT = 5;
}
private static final String TAG = "WebvttCueBuilder";
private long startTime;
private long endTime;
@Nullable private CharSequence text;
@TextAlignment private int textAlignment;
private float line;
// Equivalent to WebVTT's snap-to-lines flag:
// https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag
@Cue.LineType private int lineType;
@Cue.AnchorType private int lineAnchor;
private float position;
@Cue.AnchorType private int positionAnchor;
private float width;
// Initialization methods
// Calling reset() is forbidden because `this` isn't initialized. This can be safely
// suppressed because reset() only assigns fields, it doesn't read any.
@SuppressWarnings("nullness:method.invocation.invalid")
public Builder() {
reset();
}
public void reset() {
startTime = 0;
endTime = 0;
text = null;
// Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment
textAlignment = TextAlignment.CENTER;
line = Cue.DIMEN_UNSET;
// Defaults to NUMBER (true): https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag
lineType = Cue.LINE_TYPE_NUMBER;
// Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-line-alignment
lineAnchor = Cue.ANCHOR_TYPE_START;
position = Cue.DIMEN_UNSET;
positionAnchor = Cue.TYPE_UNSET;
// Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-size
width = 1.0f;
}
// Construction methods.
public WebvttCueInfo build() {
line = computeLine(line, lineType);
if (position == Cue.DIMEN_UNSET) {
position = derivePosition(textAlignment);
}
if (positionAnchor == Cue.TYPE_UNSET) {
positionAnchor = derivePositionAnchor(textAlignment);
}
width = Math.min(width, deriveMaxSize(positionAnchor, position));
return new WebvttCueInfo(
startTime,
endTime,
Assertions.checkNotNull(text),
convertTextAlignment(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 text) {
this.text = text;
return this;
}
public Builder setTextAlignment(@TextAlignment int textAlignment) {
this.textAlignment = textAlignment;
return this;
}
public Builder setLine(float line) {
this.line = line;
return this;
}
public Builder setLineType(@Cue.LineType int lineType) {
this.lineType = lineType;
return this;
}
public Builder setLineAnchor(@Cue.AnchorType int lineAnchor) {
this.lineAnchor = lineAnchor;
return this;
}
public Builder setPosition(float position) {
this.position = position;
return this;
}
public Builder setPositionAnchor(@Cue.AnchorType int positionAnchor) {
this.positionAnchor = positionAnchor;
return this;
}
public Builder setWidth(float width) {
this.width = width;
return this;
}
// https://www.w3.org/TR/webvtt1/#webvtt-cue-line
private static float computeLine(float line, @Cue.LineType int lineType) {
if (line != Cue.DIMEN_UNSET
&& lineType == Cue.LINE_TYPE_FRACTION
&& (line < 0.0f || line > 1.0f)) {
return 1.0f; // Step 1
} else if (line != Cue.DIMEN_UNSET) {
// Step 2: Do nothing, line is already correct.
return line;
} else if (lineType == Cue.LINE_TYPE_FRACTION) {
return 1.0f; // Step 3
} else {
// Steps 4 - 10 (stacking multiple simultaneous cues) are handled by
// WebvttSubtitle.getCues(long) and WebvttSubtitle.isNormal(Cue).
return Cue.DIMEN_UNSET;
}
}
// https://www.w3.org/TR/webvtt1/#webvtt-cue-position
private static float derivePosition(@TextAlignment int textAlignment) {
switch (textAlignment) {
case TextAlignment.LEFT:
return 0.0f;
case TextAlignment.RIGHT:
return 1.0f;
case TextAlignment.START:
case TextAlignment.CENTER:
case TextAlignment.END:
default:
return DEFAULT_POSITION;
}
}
// https://www.w3.org/TR/webvtt1/#webvtt-cue-position-alignment
@Cue.AnchorType
private static int derivePositionAnchor(@TextAlignment int textAlignment) {
switch (textAlignment) {
case TextAlignment.LEFT:
case TextAlignment.START:
return Cue.ANCHOR_TYPE_START;
case TextAlignment.RIGHT:
case TextAlignment.END:
return Cue.ANCHOR_TYPE_END;
case TextAlignment.CENTER:
default:
return Cue.ANCHOR_TYPE_MIDDLE;
}
}
@Nullable
private static Alignment convertTextAlignment(@TextAlignment int textAlignment) {
switch (textAlignment) {
case TextAlignment.START:
case TextAlignment.LEFT:
return Alignment.ALIGN_NORMAL;
case TextAlignment.CENTER:
return Alignment.ALIGN_CENTER;
case TextAlignment.END:
case TextAlignment.RIGHT:
return Alignment.ALIGN_OPPOSITE;
default:
Log.w(TAG, "Unknown textAlignment: " + textAlignment);
return null;
}
}
// Step 2 here: https://www.w3.org/TR/webvtt1/#processing-cue-settings
private static float deriveMaxSize(@Cue.AnchorType int positionAnchor, float position) {
switch (positionAnchor) {
case Cue.ANCHOR_TYPE_START:
return 1.0f - position;
case Cue.ANCHOR_TYPE_END:
return position;
case Cue.ANCHOR_TYPE_MIDDLE:
if (position <= 0.5f) {
return position * 2;
} else {
return (1.0f - position) * 2;
}
case Cue.TYPE_UNSET:
default:
throw new IllegalStateException(String.valueOf(positionAnchor));
}
}
public WebvttCueInfo(Cue cue, long startTimeUs, long endTimeUs) {
this.cue = cue;
this.startTime = startTimeUs;
this.endTime = endTimeUs;
}
}
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.text.webvtt;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
import com.google.android.exoplayer2.text.Subtitle;
......@@ -40,28 +41,20 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder {
private static final String COMMENT_START = "NOTE";
private static final String STYLE_START = "STYLE";
private final WebvttCueParser cueParser;
private final ParsableByteArray parsableWebvttData;
private final WebvttCueInfo.Builder webvttCueBuilder;
private final CssParser cssParser;
private final List<WebvttCssStyle> definedStyles;
public WebvttDecoder() {
super("WebvttDecoder");
cueParser = new WebvttCueParser();
parsableWebvttData = new ParsableByteArray();
webvttCueBuilder = new WebvttCueInfo.Builder();
cssParser = new CssParser();
definedStyles = new ArrayList<>();
}
@Override
protected Subtitle decode(byte[] bytes, int length, boolean reset)
throws SubtitleDecoderException {
parsableWebvttData.reset(bytes, length);
// Initialization for consistent starting state.
webvttCueBuilder.reset();
definedStyles.clear();
List<WebvttCssStyle> definedStyles = new ArrayList<>();
// Validate the first line of the header, and skip the remainder.
try {
......@@ -83,9 +76,10 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder {
parsableWebvttData.readLine(); // Consume the "STYLE" header.
definedStyles.addAll(cssParser.parseBlock(parsableWebvttData));
} else if (event == EVENT_CUE) {
if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, definedStyles)) {
cueInfos.add(webvttCueBuilder.build());
webvttCueBuilder.reset();
@Nullable
WebvttCueInfo cueInfo = WebvttCueParser.parseCue(parsableWebvttData, definedStyles);
if (cueInfo != null) {
cueInfos.add(cueInfo);
}
}
}
......
......@@ -80,9 +80,9 @@ import java.util.List;
// individual cues, but tweaking their `line` value):
// https://www.w3.org/TR/webvtt1/#cue-computed-line
if (isNormal(cue)) {
// we want to merge all of the normal cues into a single cue to ensure they are drawn
// We want to merge all of the normal cues into a single cue to ensure they are drawn
// correctly (i.e. don't overlap) and to emulate roll-up, but only if there are multiple
// normal cues, otherwise we can just append the single normal cue
// normal cues, otherwise we can just append the single normal cue.
if (firstNormalCue == null) {
firstNormalCue = cue;
} else if (normalCueTextBuilder == null) {
......@@ -100,10 +100,10 @@ import java.util.List;
}
}
if (normalCueTextBuilder != null) {
// there were multiple normal cues, so create a new cue with all of the text
list.add(new WebvttCueInfo.Builder().setText(normalCueTextBuilder).build().cue);
// There were multiple normal cues, so create a new cue with all of the text.
list.add(WebvttCueParser.newCueForText(normalCueTextBuilder));
} else if (firstNormalCue != null) {
// there was only a single normal cue, so just add it to the list
// There was only a single normal cue, so just add it to the list.
list.add(firstNormalCue);
}
return list;
......@@ -116,6 +116,6 @@ import java.util.List;
* @return Whether this cue should be placed in the default position.
*/
private static boolean isNormal(Cue cue) {
return (cue.line == Cue.DIMEN_UNSET && cue.position == WebvttCueInfo.DEFAULT_POSITION);
return (cue.line == Cue.DIMEN_UNSET && cue.position == WebvttCueParser.DEFAULT_POSITION);
}
}
......@@ -92,7 +92,7 @@ public final class Mp4WebvttDecoderTest {
Mp4WebvttDecoder decoder = new Mp4WebvttDecoder();
Subtitle result = decoder.decode(SINGLE_CUE_SAMPLE, SINGLE_CUE_SAMPLE.length, false);
// Line feed must be trimmed by the decoder
Cue expectedCue = new WebvttCueInfo.Builder().setText("Hello World").build().cue;
Cue expectedCue = WebvttCueParser.newCueForText("Hello World");
assertMp4WebvttSubtitleEquals(result, expectedCue);
}
......@@ -100,8 +100,8 @@ public final class Mp4WebvttDecoderTest {
public void testTwoCuesSample() throws SubtitleDecoderException {
Mp4WebvttDecoder decoder = new Mp4WebvttDecoder();
Subtitle result = decoder.decode(DOUBLE_CUE_SAMPLE, DOUBLE_CUE_SAMPLE.length, false);
Cue firstExpectedCue = new WebvttCueInfo.Builder().setText("Hello World").build().cue;
Cue secondExpectedCue = new WebvttCueInfo.Builder().setText("Bye Bye").build().cue;
Cue firstExpectedCue = WebvttCueParser.newCueForText("Hello World");
Cue secondExpectedCue = WebvttCueParser.newCueForText("Bye Bye");
assertMp4WebvttSubtitleEquals(result, firstExpectedCue, secondExpectedCue);
}
......
......@@ -217,13 +217,7 @@ public final class WebvttCueParserTest {
}
private static Spanned parseCueText(String string) {
WebvttCueInfo.Builder builder = new WebvttCueInfo.Builder();
WebvttCueParser.parseCueText(null, string, builder, Collections.emptyList());
return (Spanned) builder.build().cue.text;
return WebvttCueParser.parseCueText(
/* id= */ null, string, /* styles= */ Collections.emptyList());
}
private static <T> T[] getSpans(Spanned text, Class<T> spanType) {
return text.getSpans(0, text.length(), spanType);
}
}
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