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
This commit is contained in:
ibaker 2019-12-18 16:48:10 +00:00 committed by Oliver Woodman
parent a8d39c1180
commit 8cf4042ddd
8 changed files with 409 additions and 517 deletions

View File

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

View File

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

View File

@ -15,11 +15,14 @@
*/
package com.google.android.exoplayer2.text.webvtt;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.graphics.Typeface;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.SpannedString;
import android.text.TextUtils;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.AlignmentSpan;
@ -30,6 +33,7 @@ import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.Cue;
@ -37,19 +41,70 @@ import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) */
public final class WebvttCueParser {
/**
* Valid values for {@link WebvttCueInfoBuilder#textAlignment}.
*
* <p>We use a custom list (and not {@link Layout.Alignment} directly) in order to include both
* {@code START}/{@code LEFT} and {@code END}/{@code RIGHT}. The distinction is important for
* {@link WebvttCueInfoBuilder#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({
TEXT_ALIGNMENT_START,
TEXT_ALIGNMENT_CENTER,
TEXT_ALIGNMENT_END,
TEXT_ALIGNMENT_LEFT,
TEXT_ALIGNMENT_RIGHT
})
private @interface TextAlignment {}
/**
* See WebVTT's <a
* href="https://www.w3.org/TR/webvtt1/#webvtt-cue-start-alignment">align:start</a>.
*/
private static final int TEXT_ALIGNMENT_START = 1;
/**
* See WebVTT's <a
* href="https://www.w3.org/TR/webvtt1/#webvtt-cue-center-alignment">align:center</a>.
*/
private static final int TEXT_ALIGNMENT_CENTER = 2;
/**
* See WebVTT's <a href="https://www.w3.org/TR/webvtt1/#webvtt-cue-end-alignment">align:end</a>.
*/
private static final int TEXT_ALIGNMENT_END = 3;
/**
* See WebVTT's <a href="https://www.w3.org/TR/webvtt1/#webvtt-cue-left-alignment">align:left</a>.
*/
private static final int TEXT_ALIGNMENT_LEFT = 4;
/**
* See WebVTT's <a
* href="https://www.w3.org/TR/webvtt1/#webvtt-cue-right-alignment">align:right</a>.
*/
private static final int TEXT_ALIGNMENT_RIGHT = 5;
public static final Pattern CUE_HEADER_PATTERN = Pattern
.compile("^(\\S+)\\s+-->\\s+(\\S+)(.*)?$");
private static final Pattern CUE_SETTING_PATTERN = Pattern.compile("(\\S+?):(\\S+)");
private static final char CHAR_LESS_THAN = '<';
@ -74,92 +129,70 @@ public final class WebvttCueParser {
private static final int STYLE_BOLD = Typeface.BOLD;
private static final int STYLE_ITALIC = Typeface.ITALIC;
/* package */ static final float DEFAULT_POSITION = 0.5f;
private static final String TAG = "WebvttCueParser";
private final StringBuilder textBuilder;
public WebvttCueParser() {
textBuilder = new StringBuilder();
}
/**
* Parses the next valid WebVTT cue in a parsable array, including timestamps, settings and text.
*
* @param webvttData Parsable WebVTT file data.
* @param builder Builder for WebVTT Cues (output parameter).
* @param styles List of styles defined by the CSS style blocks preceding the cues.
* @return Whether a valid Cue was found.
* @return The parsed cue info, or null if no valid cue was found.
*/
public boolean parseCue(
ParsableByteArray webvttData, WebvttCueInfo.Builder builder, List<WebvttCssStyle> styles) {
@Nullable
public static WebvttCueInfo parseCue(ParsableByteArray webvttData, List<WebvttCssStyle> styles) {
@Nullable String firstLine = webvttData.readLine();
if (firstLine == null) {
return false;
return null;
}
Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(firstLine);
if (cueHeaderMatcher.matches()) {
// We have found the timestamps in the first line. No id present.
return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styles);
return parseCue(null, cueHeaderMatcher, webvttData, styles);
}
// The first line is not the timestamps, but could be the cue id.
@Nullable String secondLine = webvttData.readLine();
if (secondLine == null) {
return false;
return null;
}
cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine);
if (cueHeaderMatcher.matches()) {
// We can do the rest of the parsing, including the id.
return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder,
styles);
return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, styles);
}
return false;
return null;
}
/**
* Parses a string containing a list of cue settings.
*
* @param cueSettingsList String containing the settings for a given cue.
* @param builder The {@link WebvttCueInfo.Builder} where incremental construction takes place.
* @return The cue settings parsed into a {@link Cue.Builder}.
*/
/* package */ static void parseCueSettingsList(
String cueSettingsList, WebvttCueInfo.Builder builder) {
// Parse the cue settings list.
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, builder);
} else if ("align".equals(name)) {
builder.setTextAlignment(parseTextAlignment(value));
} else if ("position".equals(name)) {
parsePositionAttribute(value, builder);
} else if ("size".equals(name)) {
builder.setWidth(WebvttParserUtil.parsePercentage(value));
} else {
Log.w(TAG, "Unknown cue setting " + name + ":" + value);
}
} catch (NumberFormatException e) {
Log.w(TAG, "Skipping bad cue setting: " + cueSettingMatcher.group());
}
}
/* package */ static Cue.Builder parseCueSettingsList(String cueSettingsList) {
WebvttCueInfoBuilder builder = new WebvttCueInfoBuilder();
parseCueSettingsList(cueSettingsList, builder);
return builder.toCueBuilder();
}
/** Create a new {@link Cue} containing {@code text} and with WebVTT default values. */
/* package */ static Cue newCueForText(CharSequence text) {
WebvttCueInfoBuilder infoBuilder = new WebvttCueInfoBuilder();
infoBuilder.text = text;
return infoBuilder.toCueBuilder().build();
}
/**
* Parses the text payload of a WebVTT Cue and applies modifications on {@link
* WebvttCueInfo.Builder}.
* Parses the text payload of a WebVTT Cue and returns it as a styled {@link SpannedString}.
*
* @param id Id of the cue, {@code null} if it is not present.
* @param id ID of the cue, {@code null} if it is not present.
* @param markup The markup text to be parsed.
* @param styles List of styles defined by the CSS style blocks preceding the cues.
* @param builder Output builder.
* @return The styled cue text.
*/
/* package */ static void parseCueText(
@Nullable String id,
String markup,
WebvttCueInfo.Builder builder,
List<WebvttCssStyle> styles) {
/* package */ static SpannedString parseCueText(
@Nullable String id, String markup, List<WebvttCssStyle> styles) {
SpannableStringBuilder spannedText = new SpannableStringBuilder();
ArrayDeque<StartTag> startTagStack = new ArrayDeque<>();
List<StyleMatch> scratchStyleMatches = new ArrayList<>();
@ -227,29 +260,31 @@ public final class WebvttCueParser {
}
applySpansForTag(id, StartTag.buildWholeCueVirtualTag(), spannedText, styles,
scratchStyleMatches);
builder.setText(spannedText);
return SpannedString.valueOf(spannedText);
}
private static boolean parseCue(
// Internal methods
@Nullable
private static WebvttCueInfo parseCue(
@Nullable String id,
Matcher cueHeaderMatcher,
ParsableByteArray webvttData,
WebvttCueInfo.Builder builder,
StringBuilder textBuilder,
List<WebvttCssStyle> styles) {
WebvttCueInfoBuilder builder = new WebvttCueInfoBuilder();
try {
// Parse the cue start and end times.
builder.setStartTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)))
.setEndTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(2)));
builder.startTimeUs = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1));
builder.endTimeUs = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(2));
} catch (NumberFormatException e) {
Log.w(TAG, "Skipping cue with bad header: " + cueHeaderMatcher.group());
return false;
return null;
}
parseCueSettingsList(cueHeaderMatcher.group(3), builder);
// Parse the cue text.
textBuilder.setLength(0);
StringBuilder textBuilder = new StringBuilder();
for (String line = webvttData.readLine();
!TextUtils.isEmpty(line);
line = webvttData.readLine()) {
@ -258,20 +293,44 @@ public final class WebvttCueParser {
}
textBuilder.append(line.trim());
}
parseCueText(id, textBuilder.toString(), builder, styles);
return true;
builder.text = parseCueText(id, textBuilder.toString(), styles);
return builder.build();
}
// Internal methods
private static void parseCueSettingsList(String cueSettingsList, WebvttCueInfoBuilder builder) {
// Parse the cue settings list.
Matcher cueSettingMatcher = CUE_SETTING_PATTERN.matcher(cueSettingsList);
private static void parseLineAttribute(String s, WebvttCueInfo.Builder builder) {
while (cueSettingMatcher.find()) {
String name = cueSettingMatcher.group(1);
String value = cueSettingMatcher.group(2);
try {
if ("line".equals(name)) {
parseLineAttribute(value, builder);
} else if ("align".equals(name)) {
builder.textAlignment = parseTextAlignment(value);
} else if ("position".equals(name)) {
parsePositionAttribute(value, builder);
} else if ("size".equals(name)) {
builder.size = WebvttParserUtil.parsePercentage(value);
} else {
Log.w(TAG, "Unknown cue setting " + name + ":" + value);
}
} catch (NumberFormatException e) {
Log.w(TAG, "Skipping bad cue setting: " + cueSettingMatcher.group());
}
}
}
private static void parseLineAttribute(String s, WebvttCueInfoBuilder builder) {
int commaIndex = s.indexOf(',');
if (commaIndex != -1) {
builder.setLineAnchor(parsePositionAnchor(s.substring(commaIndex + 1)));
builder.lineAnchor = parsePositionAnchor(s.substring(commaIndex + 1));
s = s.substring(0, commaIndex);
}
if (s.endsWith("%")) {
builder.setLine(WebvttParserUtil.parsePercentage(s)).setLineType(Cue.LINE_TYPE_FRACTION);
builder.line = WebvttParserUtil.parsePercentage(s);
builder.lineType = Cue.LINE_TYPE_FRACTION;
} else {
int lineNumber = Integer.parseInt(s);
if (lineNumber < 0) {
@ -279,17 +338,18 @@ public final class WebvttCueParser {
// Cue defines it to be the first row that's not visible.
lineNumber--;
}
builder.setLine(lineNumber).setLineType(Cue.LINE_TYPE_NUMBER);
builder.line = lineNumber;
builder.lineType = Cue.LINE_TYPE_NUMBER;
}
}
private static void parsePositionAttribute(String s, WebvttCueInfo.Builder builder) {
private static void parsePositionAttribute(String s, WebvttCueInfoBuilder builder) {
int commaIndex = s.indexOf(',');
if (commaIndex != -1) {
builder.setPositionAnchor(parsePositionAnchor(s.substring(commaIndex + 1)));
builder.positionAnchor = parsePositionAnchor(s.substring(commaIndex + 1));
s = s.substring(0, commaIndex);
}
builder.setPosition(WebvttParserUtil.parsePercentage(s));
builder.position = WebvttParserUtil.parsePercentage(s);
}
@Cue.AnchorType
@ -308,24 +368,24 @@ public final class WebvttCueParser {
}
}
@WebvttCueInfo.Builder.TextAlignment
@TextAlignment
private static int parseTextAlignment(String s) {
switch (s) {
case "start":
return WebvttCueInfo.Builder.TextAlignment.START;
return TEXT_ALIGNMENT_START;
case "left":
return WebvttCueInfo.Builder.TextAlignment.LEFT;
return TEXT_ALIGNMENT_LEFT;
case "center":
case "middle":
return WebvttCueInfo.Builder.TextAlignment.CENTER;
return TEXT_ALIGNMENT_CENTER;
case "end":
return WebvttCueInfo.Builder.TextAlignment.END;
return TEXT_ALIGNMENT_END;
case "right":
return WebvttCueInfo.Builder.TextAlignment.RIGHT;
return TEXT_ALIGNMENT_RIGHT;
default:
Log.w(TAG, "Invalid alignment value: " + s);
// Default value: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment
return WebvttCueInfo.Builder.TextAlignment.CENTER;
return TEXT_ALIGNMENT_CENTER;
}
}
@ -490,6 +550,151 @@ public final class WebvttCueParser {
Collections.sort(output);
}
private static final class WebvttCueInfoBuilder {
public long startTimeUs;
public long endTimeUs;
public @MonotonicNonNull CharSequence text;
@TextAlignment public int textAlignment;
public float line;
// Equivalent to WebVTT's snap-to-lines flag:
// https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag
@Cue.LineType public int lineType;
@Cue.AnchorType public int lineAnchor;
public float position;
@Cue.AnchorType public int positionAnchor;
public float size;
public WebvttCueInfoBuilder() {
startTimeUs = 0;
endTimeUs = 0;
// Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment
textAlignment = TEXT_ALIGNMENT_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
size = 1.0f;
}
public WebvttCueInfo build() {
return new WebvttCueInfo(toCueBuilder().build(), startTimeUs, endTimeUs);
}
public Cue.Builder toCueBuilder() {
float position =
this.position != Cue.DIMEN_UNSET ? this.position : derivePosition(textAlignment);
@Cue.AnchorType
int positionAnchor =
this.positionAnchor != Cue.TYPE_UNSET
? this.positionAnchor
: derivePositionAnchor(textAlignment);
Cue.Builder cueBuilder =
new Cue.Builder()
.setTextAlignment(convertTextAlignment(textAlignment))
.setLine(computeLine(line, lineType), lineType)
.setLineAnchor(lineAnchor)
.setPosition(position)
.setPositionAnchor(positionAnchor)
.setSize(Math.min(size, deriveMaxSize(positionAnchor, position)));
if (text != null) {
cueBuilder.setText(text);
}
return cueBuilder;
}
// 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 TEXT_ALIGNMENT_LEFT:
return 0.0f;
case TEXT_ALIGNMENT_RIGHT:
return 1.0f;
case TEXT_ALIGNMENT_START:
case TEXT_ALIGNMENT_CENTER:
case TEXT_ALIGNMENT_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 TEXT_ALIGNMENT_LEFT:
case TEXT_ALIGNMENT_START:
return Cue.ANCHOR_TYPE_START;
case TEXT_ALIGNMENT_RIGHT:
case TEXT_ALIGNMENT_END:
return Cue.ANCHOR_TYPE_END;
case TEXT_ALIGNMENT_CENTER:
default:
return Cue.ANCHOR_TYPE_MIDDLE;
}
}
@Nullable
private static Layout.Alignment convertTextAlignment(@TextAlignment int textAlignment) {
switch (textAlignment) {
case TEXT_ALIGNMENT_START:
case TEXT_ALIGNMENT_LEFT:
return Layout.Alignment.ALIGN_NORMAL;
case TEXT_ALIGNMENT_CENTER:
return Layout.Alignment.ALIGN_CENTER;
case TEXT_ALIGNMENT_END:
case TEXT_ALIGNMENT_RIGHT:
return Layout.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));
}
}
}
private static final class StyleMatch implements Comparable<StyleMatch> {
public final int score;
@ -550,5 +755,4 @@ public final class WebvttCueParser {
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@ import static java.lang.Long.MAX_VALUE;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.text.Cue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
@ -38,68 +38,41 @@ public class WebvttSubtitleTest {
private static final WebvttSubtitle emptySubtitle = new WebvttSubtitle(Collections.emptyList());
private static final WebvttSubtitle simpleSubtitle;
private static final WebvttSubtitle simpleSubtitle =
new WebvttSubtitle(
Arrays.asList(
new WebvttCueInfo(
WebvttCueParser.newCueForText(FIRST_SUBTITLE_STRING),
/* startTimeUs= */ 1_000_000,
/* endTimeUs= */ 2_000_000),
new WebvttCueInfo(
WebvttCueParser.newCueForText(SECOND_SUBTITLE_STRING),
/* startTimeUs= */ 3_000_000,
/* endTimeUs= */ 4_000_000)));
static {
ArrayList<WebvttCueInfo> simpleSubtitleCues = new ArrayList<>();
WebvttCueInfo firstCue =
new WebvttCueInfo.Builder()
.setStartTime(1000000)
.setEndTime(2000000)
.setText(FIRST_SUBTITLE_STRING)
.build();
simpleSubtitleCues.add(firstCue);
WebvttCueInfo secondCue =
new WebvttCueInfo.Builder()
.setStartTime(3000000)
.setEndTime(4000000)
.setText(SECOND_SUBTITLE_STRING)
.build();
simpleSubtitleCues.add(secondCue);
simpleSubtitle = new WebvttSubtitle(simpleSubtitleCues);
}
private static final WebvttSubtitle overlappingSubtitle =
new WebvttSubtitle(
Arrays.asList(
new WebvttCueInfo(
WebvttCueParser.newCueForText(FIRST_SUBTITLE_STRING),
/* startTimeUs= */ 1_000_000,
/* endTimeUs= */ 3_000_000),
new WebvttCueInfo(
WebvttCueParser.newCueForText(SECOND_SUBTITLE_STRING),
/* startTimeUs= */ 2_000_000,
/* endTimeUs= */ 4_000_000)));
private static final WebvttSubtitle overlappingSubtitle;
static {
ArrayList<WebvttCueInfo> overlappingSubtitleCues = new ArrayList<>();
WebvttCueInfo firstCue =
new WebvttCueInfo.Builder()
.setStartTime(1000000)
.setEndTime(3000000)
.setText(FIRST_SUBTITLE_STRING)
.build();
overlappingSubtitleCues.add(firstCue);
WebvttCueInfo secondCue =
new WebvttCueInfo.Builder()
.setStartTime(2000000)
.setEndTime(4000000)
.setText(SECOND_SUBTITLE_STRING)
.build();
overlappingSubtitleCues.add(secondCue);
overlappingSubtitle = new WebvttSubtitle(overlappingSubtitleCues);
}
private static final WebvttSubtitle nestedSubtitle;
static {
ArrayList<WebvttCueInfo> nestedSubtitleCues = new ArrayList<>();
WebvttCueInfo firstCue =
new WebvttCueInfo.Builder()
.setStartTime(1000000)
.setEndTime(4000000)
.setText(FIRST_SUBTITLE_STRING)
.build();
nestedSubtitleCues.add(firstCue);
WebvttCueInfo secondCue =
new WebvttCueInfo.Builder()
.setStartTime(2000000)
.setEndTime(3000000)
.setText(SECOND_SUBTITLE_STRING)
.build();
nestedSubtitleCues.add(secondCue);
nestedSubtitle = new WebvttSubtitle(nestedSubtitleCues);
}
private static final WebvttSubtitle nestedSubtitle =
new WebvttSubtitle(
Arrays.asList(
new WebvttCueInfo(
WebvttCueParser.newCueForText(FIRST_SUBTITLE_STRING),
/* startTimeUs= */ 1_000_000,
/* endTimeUs= */ 4_000_000),
new WebvttCueInfo(
WebvttCueParser.newCueForText(SECOND_SUBTITLE_STRING),
/* startTimeUs= */ 2_000_000,
/* endTimeUs= */ 3_000_000)));
@Test
public void testEventCount() {
@ -123,27 +96,27 @@ public class WebvttSubtitleTest {
public void testSimpleSubtitleText() {
// Test before first subtitle
assertSingleCueEmpty(simpleSubtitle.getCues(0));
assertSingleCueEmpty(simpleSubtitle.getCues(500000));
assertSingleCueEmpty(simpleSubtitle.getCues(999999));
assertSingleCueEmpty(simpleSubtitle.getCues(500_000));
assertSingleCueEmpty(simpleSubtitle.getCues(999_999));
// Test first subtitle
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1000000));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1500000));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1999999));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1_000_000));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1_500_000));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getCues(1_999_999));
// Test after first subtitle, before second subtitle
assertSingleCueEmpty(simpleSubtitle.getCues(2000000));
assertSingleCueEmpty(simpleSubtitle.getCues(2500000));
assertSingleCueEmpty(simpleSubtitle.getCues(2999999));
assertSingleCueEmpty(simpleSubtitle.getCues(2_000_000));
assertSingleCueEmpty(simpleSubtitle.getCues(2_500_000));
assertSingleCueEmpty(simpleSubtitle.getCues(2_999_999));
// Test second subtitle
assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3000000));
assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3500000));
assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3999999));
assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3_000_000));
assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3_500_000));
assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getCues(3_999_999));
// Test after second subtitle
assertSingleCueEmpty(simpleSubtitle.getCues(4000000));
assertSingleCueEmpty(simpleSubtitle.getCues(4500000));
assertSingleCueEmpty(simpleSubtitle.getCues(4_000_000));
assertSingleCueEmpty(simpleSubtitle.getCues(4_500_000));
assertSingleCueEmpty(simpleSubtitle.getCues(Long.MAX_VALUE));
}
@ -161,30 +134,30 @@ public class WebvttSubtitleTest {
public void testOverlappingSubtitleText() {
// Test before first subtitle
assertSingleCueEmpty(overlappingSubtitle.getCues(0));
assertSingleCueEmpty(overlappingSubtitle.getCues(500000));
assertSingleCueEmpty(overlappingSubtitle.getCues(999999));
assertSingleCueEmpty(overlappingSubtitle.getCues(500_000));
assertSingleCueEmpty(overlappingSubtitle.getCues(999_999));
// Test first subtitle
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1000000));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1500000));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1999999));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1_000_000));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1_500_000));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getCues(1_999_999));
// Test after first and second subtitle
assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING,
overlappingSubtitle.getCues(2000000));
assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING,
overlappingSubtitle.getCues(2500000));
assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING,
overlappingSubtitle.getCues(2999999));
assertSingleCueTextEquals(
FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(2_000_000));
assertSingleCueTextEquals(
FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(2_500_000));
assertSingleCueTextEquals(
FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(2_999_999));
// Test second subtitle
assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3000000));
assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3500000));
assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3999999));
assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3_000_000));
assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3_500_000));
assertSingleCueTextEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getCues(3_999_999));
// Test after second subtitle
assertSingleCueEmpty(overlappingSubtitle.getCues(4000000));
assertSingleCueEmpty(overlappingSubtitle.getCues(4500000));
assertSingleCueEmpty(overlappingSubtitle.getCues(4_000_000));
assertSingleCueEmpty(overlappingSubtitle.getCues(4_500_000));
assertSingleCueEmpty(overlappingSubtitle.getCues(Long.MAX_VALUE));
}
@ -202,61 +175,61 @@ public class WebvttSubtitleTest {
public void testNestedSubtitleText() {
// Test before first subtitle
assertSingleCueEmpty(nestedSubtitle.getCues(0));
assertSingleCueEmpty(nestedSubtitle.getCues(500000));
assertSingleCueEmpty(nestedSubtitle.getCues(999999));
assertSingleCueEmpty(nestedSubtitle.getCues(500_000));
assertSingleCueEmpty(nestedSubtitle.getCues(999_999));
// Test first subtitle
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1000000));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1500000));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1999999));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1_000_000));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1_500_000));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(1_999_999));
// Test after first and second subtitle
assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2000000));
assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2500000));
assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2999999));
assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2_000_000));
assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2_500_000));
assertSingleCueTextEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getCues(2_999_999));
// Test first subtitle
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3000000));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3500000));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3999999));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3_000_000));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3_500_000));
assertSingleCueTextEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getCues(3_999_999));
// Test after second subtitle
assertSingleCueEmpty(nestedSubtitle.getCues(4000000));
assertSingleCueEmpty(nestedSubtitle.getCues(4500000));
assertSingleCueEmpty(nestedSubtitle.getCues(4_000_000));
assertSingleCueEmpty(nestedSubtitle.getCues(4_500_000));
assertSingleCueEmpty(nestedSubtitle.getCues(Long.MAX_VALUE));
}
private void testSubtitleEventTimesHelper(WebvttSubtitle subtitle) {
assertThat(subtitle.getEventTime(0)).isEqualTo(1000000);
assertThat(subtitle.getEventTime(1)).isEqualTo(2000000);
assertThat(subtitle.getEventTime(2)).isEqualTo(3000000);
assertThat(subtitle.getEventTime(3)).isEqualTo(4000000);
assertThat(subtitle.getEventTime(0)).isEqualTo(1_000_000);
assertThat(subtitle.getEventTime(1)).isEqualTo(2_000_000);
assertThat(subtitle.getEventTime(2)).isEqualTo(3_000_000);
assertThat(subtitle.getEventTime(3)).isEqualTo(4_000_000);
}
private void testSubtitleEventIndicesHelper(WebvttSubtitle subtitle) {
// Test first event
assertThat(subtitle.getNextEventTimeIndex(0)).isEqualTo(0);
assertThat(subtitle.getNextEventTimeIndex(500000)).isEqualTo(0);
assertThat(subtitle.getNextEventTimeIndex(999999)).isEqualTo(0);
assertThat(subtitle.getNextEventTimeIndex(500_000)).isEqualTo(0);
assertThat(subtitle.getNextEventTimeIndex(999_999)).isEqualTo(0);
// Test second event
assertThat(subtitle.getNextEventTimeIndex(1000000)).isEqualTo(1);
assertThat(subtitle.getNextEventTimeIndex(1500000)).isEqualTo(1);
assertThat(subtitle.getNextEventTimeIndex(1999999)).isEqualTo(1);
assertThat(subtitle.getNextEventTimeIndex(1_000_000)).isEqualTo(1);
assertThat(subtitle.getNextEventTimeIndex(1_500_000)).isEqualTo(1);
assertThat(subtitle.getNextEventTimeIndex(1_999_999)).isEqualTo(1);
// Test third event
assertThat(subtitle.getNextEventTimeIndex(2000000)).isEqualTo(2);
assertThat(subtitle.getNextEventTimeIndex(2500000)).isEqualTo(2);
assertThat(subtitle.getNextEventTimeIndex(2999999)).isEqualTo(2);
assertThat(subtitle.getNextEventTimeIndex(2_000_000)).isEqualTo(2);
assertThat(subtitle.getNextEventTimeIndex(2_500_000)).isEqualTo(2);
assertThat(subtitle.getNextEventTimeIndex(2_999_999)).isEqualTo(2);
// Test fourth event
assertThat(subtitle.getNextEventTimeIndex(3000000)).isEqualTo(3);
assertThat(subtitle.getNextEventTimeIndex(3500000)).isEqualTo(3);
assertThat(subtitle.getNextEventTimeIndex(3999999)).isEqualTo(3);
assertThat(subtitle.getNextEventTimeIndex(3_000_000)).isEqualTo(3);
assertThat(subtitle.getNextEventTimeIndex(3_500_000)).isEqualTo(3);
assertThat(subtitle.getNextEventTimeIndex(3_999_999)).isEqualTo(3);
// Test null event (i.e. look for events after the last event)
assertThat(subtitle.getNextEventTimeIndex(4000000)).isEqualTo(INDEX_UNSET);
assertThat(subtitle.getNextEventTimeIndex(4500000)).isEqualTo(INDEX_UNSET);
assertThat(subtitle.getNextEventTimeIndex(4_000_000)).isEqualTo(INDEX_UNSET);
assertThat(subtitle.getNextEventTimeIndex(4_500_000)).isEqualTo(INDEX_UNSET);
assertThat(subtitle.getNextEventTimeIndex(MAX_VALUE)).isEqualTo(INDEX_UNSET);
}
@ -268,5 +241,4 @@ public class WebvttSubtitleTest {
assertThat(cues).hasSize(1);
assertThat(cues.get(0).text.toString()).isEqualTo(expected);
}
}