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