From 8cf4042ddd8f83d37c025023e533b55a7f68a061 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 18 Dec 2019 16:48:10 +0000 Subject: [PATCH] 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 --- .../text/webvtt/Mp4WebvttDecoder.java | 25 +- .../exoplayer2/text/webvtt/WebvttCueInfo.java | 285 +------------- .../text/webvtt/WebvttCueParser.java | 356 ++++++++++++++---- .../exoplayer2/text/webvtt/WebvttDecoder.java | 18 +- .../text/webvtt/WebvttSubtitle.java | 12 +- .../text/webvtt/Mp4WebvttDecoderTest.java | 6 +- .../text/webvtt/WebvttCueParserTest.java | 10 +- .../text/webvtt/WebvttSubtitleTest.java | 214 +++++------ 8 files changed, 409 insertions(+), 517 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java index 81ff0fdd65..82023e6c58 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java @@ -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); } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueInfo.java index c57d14ac5c..b04e06d744 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueInfo.java @@ -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)}. - * - *

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)}. - * - *

These correspond to the valid values for the 'align' cue setting in the WebVTT spec. - */ - @Documented - @Retention(SOURCE) - @IntDef({ - TextAlignment.START, - TextAlignment.CENTER, - TextAlignment.END, - TextAlignment.LEFT, - TextAlignment.RIGHT - }) - public @interface TextAlignment { - /** - * See WebVTT's align:start. - */ - int START = 1; - /** - * See WebVTT's align:center. - */ - int CENTER = 2; - /** - * See WebVTT's align:end. - */ - int END = 3; - /** - * See WebVTT's align:left. - */ - int LEFT = 4; - /** - * See WebVTT's align:right. - */ - 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; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index e6fa78ca65..565d324828 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -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}. + * + *

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)}. + * + *

These correspond to the valid values for the 'align' cue setting in the WebVTT spec. + */ + @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 align:start. + */ + private static final int TEXT_ALIGNMENT_START = 1; + + /** + * See WebVTT's align:center. + */ + private static final int TEXT_ALIGNMENT_CENTER = 2; + + /** + * See WebVTT's align:end. + */ + private static final int TEXT_ALIGNMENT_END = 3; + + /** + * See WebVTT's align:left. + */ + private static final int TEXT_ALIGNMENT_LEFT = 4; + + /** + * See WebVTT's align:right. + */ + 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 styles) { + @Nullable + public static WebvttCueInfo parseCue(ParsableByteArray webvttData, List 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 styles) { + /* package */ static SpannedString parseCueText( + @Nullable String id, String markup, List styles) { SpannableStringBuilder spannedText = new SpannableStringBuilder(); ArrayDeque startTagStack = new ArrayDeque<>(); List 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 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 { public final int score; @@ -550,5 +755,4 @@ public final class WebvttCueParser { } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java index 6c1d61d126..fe36770aee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java @@ -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 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 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); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java index 83c588fb77..49ee73ea5a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java @@ -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); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java index 69d7caa832..5f91193699 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java @@ -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); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java index ebaec594f1..d23ed00e95 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java @@ -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[] getSpans(Spanned text, Class spanType) { - return text.getSpans(0, text.length(), spanType); - } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java index 621751db94..61c6394db4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java @@ -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 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 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 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); } - }