diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java index 946af76e53..8880a76e1e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -24,10 +24,11 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -/** - * Contains information about a specific cue, including textual content and formatting data. - */ -public class Cue { +/** Contains information about a specific cue, including textual content and formatting data. */ +// This class shouldn't be sub-classed. If a subtitle format needs additional fields, either they +// should be generic enough to be added here, or the format-specific decoder should pass the +// information around in a sidecar object. +public final class Cue { /** The empty cue. */ public static final Cue EMPTY = new Cue(""); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java deleted file mode 100644 index e04094a8dc..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.text.cea; - -import android.text.Layout.Alignment; -import androidx.annotation.NonNull; -import com.google.android.exoplayer2.text.Cue; - -/** - * A {@link Cue} for CEA-708. - */ -/* package */ final class Cea708Cue extends Cue implements Comparable { - - /** - * The priority of the cue box. - */ - public final int priority; - - /** - * @param text See {@link #text}. - * @param textAlignment See {@link #textAlignment}. - * @param line See {@link #line}. - * @param lineType See {@link #lineType}. - * @param lineAnchor See {@link #lineAnchor}. - * @param position See {@link #position}. - * @param positionAnchor See {@link #positionAnchor}. - * @param size See {@link #size}. - * @param windowColorSet See {@link #windowColorSet}. - * @param windowColor See {@link #windowColor}. - * @param priority See (@link #priority}. - */ - public Cea708Cue(CharSequence text, Alignment textAlignment, float line, @LineType int lineType, - @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, - boolean windowColorSet, int windowColor, int priority) { - super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, - windowColorSet, windowColor); - this.priority = priority; - } - - @Override - public int compareTo(@NonNull Cea708Cue other) { - if (other.priority < priority) { - return -1; - } else if (other.priority > priority) { - return 1; - } - return 0; - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index 4391bc0bf0..4eaf65c721 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -144,9 +144,9 @@ public final class Cea708Decoder extends CeaDecoder { private final ParsableBitArray serviceBlockPacket; private final int selectedServiceNumber; - private final CueBuilder[] cueBuilders; + private final CueInfoBuilder[] cueInfoBuilders; - private CueBuilder currentCueBuilder; + private CueInfoBuilder currentCueInfoBuilder; private List cues; private List lastCues; @@ -159,12 +159,12 @@ public final class Cea708Decoder extends CeaDecoder { serviceBlockPacket = new ParsableBitArray(); selectedServiceNumber = accessibilityChannel == Format.NO_VALUE ? 1 : accessibilityChannel; - cueBuilders = new CueBuilder[NUM_WINDOWS]; + cueInfoBuilders = new CueInfoBuilder[NUM_WINDOWS]; for (int i = 0; i < NUM_WINDOWS; i++) { - cueBuilders[i] = new CueBuilder(); + cueInfoBuilders[i] = new CueInfoBuilder(); } - currentCueBuilder = cueBuilders[0]; + currentCueInfoBuilder = cueInfoBuilders[0]; resetCueBuilders(); } @@ -179,7 +179,7 @@ public final class Cea708Decoder extends CeaDecoder { cues = null; lastCues = null; currentWindow = 0; - currentCueBuilder = cueBuilders[currentWindow]; + currentCueInfoBuilder = cueInfoBuilders[currentWindow]; resetCueBuilders(); currentDtvCcPacket = null; } @@ -348,13 +348,13 @@ public final class Cea708Decoder extends CeaDecoder { cues = getDisplayCues(); break; case COMMAND_BS: - currentCueBuilder.backspace(); + currentCueInfoBuilder.backspace(); break; case COMMAND_FF: resetCueBuilders(); break; case COMMAND_CR: - currentCueBuilder.append('\n'); + currentCueInfoBuilder.append('\n'); break; case COMMAND_HCR: // TODO: Add support for this command. @@ -386,42 +386,42 @@ public final class Cea708Decoder extends CeaDecoder { window = (command - COMMAND_CW0); if (currentWindow != window) { currentWindow = window; - currentCueBuilder = cueBuilders[window]; + currentCueInfoBuilder = cueInfoBuilders[window]; } break; case COMMAND_CLW: for (int i = 1; i <= NUM_WINDOWS; i++) { if (serviceBlockPacket.readBit()) { - cueBuilders[NUM_WINDOWS - i].clear(); + cueInfoBuilders[NUM_WINDOWS - i].clear(); } } break; case COMMAND_DSW: for (int i = 1; i <= NUM_WINDOWS; i++) { if (serviceBlockPacket.readBit()) { - cueBuilders[NUM_WINDOWS - i].setVisibility(true); + cueInfoBuilders[NUM_WINDOWS - i].setVisibility(true); } } break; case COMMAND_HDW: for (int i = 1; i <= NUM_WINDOWS; i++) { if (serviceBlockPacket.readBit()) { - cueBuilders[NUM_WINDOWS - i].setVisibility(false); + cueInfoBuilders[NUM_WINDOWS - i].setVisibility(false); } } break; case COMMAND_TGW: for (int i = 1; i <= NUM_WINDOWS; i++) { if (serviceBlockPacket.readBit()) { - CueBuilder cueBuilder = cueBuilders[NUM_WINDOWS - i]; - cueBuilder.setVisibility(!cueBuilder.isVisible()); + CueInfoBuilder cueInfoBuilder = cueInfoBuilders[NUM_WINDOWS - i]; + cueInfoBuilder.setVisibility(!cueInfoBuilder.isVisible()); } } break; case COMMAND_DLW: for (int i = 1; i <= NUM_WINDOWS; i++) { if (serviceBlockPacket.readBit()) { - cueBuilders[NUM_WINDOWS - i].reset(); + cueInfoBuilders[NUM_WINDOWS - i].reset(); } } break; @@ -436,7 +436,7 @@ public final class Cea708Decoder extends CeaDecoder { resetCueBuilders(); break; case COMMAND_SPA: - if (!currentCueBuilder.isDefined()) { + if (!currentCueInfoBuilder.isDefined()) { // ignore this command if the current window/cue isn't defined serviceBlockPacket.skipBits(16); } else { @@ -444,7 +444,7 @@ public final class Cea708Decoder extends CeaDecoder { } break; case COMMAND_SPC: - if (!currentCueBuilder.isDefined()) { + if (!currentCueInfoBuilder.isDefined()) { // ignore this command if the current window/cue isn't defined serviceBlockPacket.skipBits(24); } else { @@ -452,7 +452,7 @@ public final class Cea708Decoder extends CeaDecoder { } break; case COMMAND_SPL: - if (!currentCueBuilder.isDefined()) { + if (!currentCueInfoBuilder.isDefined()) { // ignore this command if the current window/cue isn't defined serviceBlockPacket.skipBits(16); } else { @@ -460,7 +460,7 @@ public final class Cea708Decoder extends CeaDecoder { } break; case COMMAND_SWA: - if (!currentCueBuilder.isDefined()) { + if (!currentCueInfoBuilder.isDefined()) { // ignore this command if the current window/cue isn't defined serviceBlockPacket.skipBits(32); } else { @@ -480,7 +480,7 @@ public final class Cea708Decoder extends CeaDecoder { // We also set the current window to the newly defined window. if (currentWindow != window) { currentWindow = window; - currentCueBuilder = cueBuilders[window]; + currentCueInfoBuilder = cueInfoBuilders[window]; } break; default: @@ -519,95 +519,95 @@ public final class Cea708Decoder extends CeaDecoder { private void handleG0Character(int characterCode) { if (characterCode == CHARACTER_MN) { - currentCueBuilder.append('\u266B'); + currentCueInfoBuilder.append('\u266B'); } else { - currentCueBuilder.append((char) (characterCode & 0xFF)); + currentCueInfoBuilder.append((char) (characterCode & 0xFF)); } } private void handleG1Character(int characterCode) { - currentCueBuilder.append((char) (characterCode & 0xFF)); + currentCueInfoBuilder.append((char) (characterCode & 0xFF)); } private void handleG2Character(int characterCode) { switch (characterCode) { case CHARACTER_TSP: - currentCueBuilder.append('\u0020'); + currentCueInfoBuilder.append('\u0020'); break; case CHARACTER_NBTSP: - currentCueBuilder.append('\u00A0'); + currentCueInfoBuilder.append('\u00A0'); break; case CHARACTER_ELLIPSIS: - currentCueBuilder.append('\u2026'); + currentCueInfoBuilder.append('\u2026'); break; case CHARACTER_BIG_CARONS: - currentCueBuilder.append('\u0160'); + currentCueInfoBuilder.append('\u0160'); break; case CHARACTER_BIG_OE: - currentCueBuilder.append('\u0152'); + currentCueInfoBuilder.append('\u0152'); break; case CHARACTER_SOLID_BLOCK: - currentCueBuilder.append('\u2588'); + currentCueInfoBuilder.append('\u2588'); break; case CHARACTER_OPEN_SINGLE_QUOTE: - currentCueBuilder.append('\u2018'); + currentCueInfoBuilder.append('\u2018'); break; case CHARACTER_CLOSE_SINGLE_QUOTE: - currentCueBuilder.append('\u2019'); + currentCueInfoBuilder.append('\u2019'); break; case CHARACTER_OPEN_DOUBLE_QUOTE: - currentCueBuilder.append('\u201C'); + currentCueInfoBuilder.append('\u201C'); break; case CHARACTER_CLOSE_DOUBLE_QUOTE: - currentCueBuilder.append('\u201D'); + currentCueInfoBuilder.append('\u201D'); break; case CHARACTER_BOLD_BULLET: - currentCueBuilder.append('\u2022'); + currentCueInfoBuilder.append('\u2022'); break; case CHARACTER_TM: - currentCueBuilder.append('\u2122'); + currentCueInfoBuilder.append('\u2122'); break; case CHARACTER_SMALL_CARONS: - currentCueBuilder.append('\u0161'); + currentCueInfoBuilder.append('\u0161'); break; case CHARACTER_SMALL_OE: - currentCueBuilder.append('\u0153'); + currentCueInfoBuilder.append('\u0153'); break; case CHARACTER_SM: - currentCueBuilder.append('\u2120'); + currentCueInfoBuilder.append('\u2120'); break; case CHARACTER_DIAERESIS_Y: - currentCueBuilder.append('\u0178'); + currentCueInfoBuilder.append('\u0178'); break; case CHARACTER_ONE_EIGHTH: - currentCueBuilder.append('\u215B'); + currentCueInfoBuilder.append('\u215B'); break; case CHARACTER_THREE_EIGHTHS: - currentCueBuilder.append('\u215C'); + currentCueInfoBuilder.append('\u215C'); break; case CHARACTER_FIVE_EIGHTHS: - currentCueBuilder.append('\u215D'); + currentCueInfoBuilder.append('\u215D'); break; case CHARACTER_SEVEN_EIGHTHS: - currentCueBuilder.append('\u215E'); + currentCueInfoBuilder.append('\u215E'); break; case CHARACTER_VERTICAL_BORDER: - currentCueBuilder.append('\u2502'); + currentCueInfoBuilder.append('\u2502'); break; case CHARACTER_UPPER_RIGHT_BORDER: - currentCueBuilder.append('\u2510'); + currentCueInfoBuilder.append('\u2510'); break; case CHARACTER_LOWER_LEFT_BORDER: - currentCueBuilder.append('\u2514'); + currentCueInfoBuilder.append('\u2514'); break; case CHARACTER_HORIZONTAL_BORDER: - currentCueBuilder.append('\u2500'); + currentCueInfoBuilder.append('\u2500'); break; case CHARACTER_LOWER_RIGHT_BORDER: - currentCueBuilder.append('\u2518'); + currentCueInfoBuilder.append('\u2518'); break; case CHARACTER_UPPER_LEFT_BORDER: - currentCueBuilder.append('\u250C'); + currentCueInfoBuilder.append('\u250C'); break; default: Log.w(TAG, "Invalid G2 character: " + characterCode); @@ -618,11 +618,11 @@ public final class Cea708Decoder extends CeaDecoder { private void handleG3Character(int characterCode) { if (characterCode == 0xA0) { - currentCueBuilder.append('\u33C4'); + currentCueInfoBuilder.append('\u33C4'); } else { Log.w(TAG, "Invalid G3 character: " + characterCode); // Substitute any unsupported G3 character with an underscore as per CEA-708 specification. - currentCueBuilder.append('_'); + currentCueInfoBuilder.append('_'); } } @@ -638,8 +638,8 @@ public final class Cea708Decoder extends CeaDecoder { int edgeType = serviceBlockPacket.readBits(3); int fontStyle = serviceBlockPacket.readBits(3); - currentCueBuilder.setPenAttributes(textTag, offset, penSize, italicsToggle, underlineToggle, - edgeType, fontStyle); + currentCueInfoBuilder.setPenAttributes( + textTag, offset, penSize, italicsToggle, underlineToggle, edgeType, fontStyle); } private void handleSetPenColor() { @@ -649,23 +649,23 @@ public final class Cea708Decoder extends CeaDecoder { int foregroundR = serviceBlockPacket.readBits(2); int foregroundG = serviceBlockPacket.readBits(2); int foregroundB = serviceBlockPacket.readBits(2); - int foregroundColor = CueBuilder.getArgbColorFromCeaColor(foregroundR, foregroundG, foregroundB, - foregroundO); + int foregroundColor = + CueInfoBuilder.getArgbColorFromCeaColor(foregroundR, foregroundG, foregroundB, foregroundO); // second byte int backgroundO = serviceBlockPacket.readBits(2); int backgroundR = serviceBlockPacket.readBits(2); int backgroundG = serviceBlockPacket.readBits(2); int backgroundB = serviceBlockPacket.readBits(2); - int backgroundColor = CueBuilder.getArgbColorFromCeaColor(backgroundR, backgroundG, backgroundB, - backgroundO); + int backgroundColor = + CueInfoBuilder.getArgbColorFromCeaColor(backgroundR, backgroundG, backgroundB, backgroundO); // third byte serviceBlockPacket.skipBits(2); // null padding int edgeR = serviceBlockPacket.readBits(2); int edgeG = serviceBlockPacket.readBits(2); int edgeB = serviceBlockPacket.readBits(2); - int edgeColor = CueBuilder.getArgbColorFromCeaColor(edgeR, edgeG, edgeB); + int edgeColor = CueInfoBuilder.getArgbColorFromCeaColor(edgeR, edgeG, edgeB); - currentCueBuilder.setPenColor(foregroundColor, backgroundColor, edgeColor); + currentCueInfoBuilder.setPenColor(foregroundColor, backgroundColor, edgeColor); } private void handleSetPenLocation() { @@ -677,7 +677,7 @@ public final class Cea708Decoder extends CeaDecoder { serviceBlockPacket.skipBits(2); int column = serviceBlockPacket.readBits(6); - currentCueBuilder.setPenLocation(row, column); + currentCueInfoBuilder.setPenLocation(row, column); } private void handleSetWindowAttributes() { @@ -687,13 +687,13 @@ public final class Cea708Decoder extends CeaDecoder { int fillR = serviceBlockPacket.readBits(2); int fillG = serviceBlockPacket.readBits(2); int fillB = serviceBlockPacket.readBits(2); - int fillColor = CueBuilder.getArgbColorFromCeaColor(fillR, fillG, fillB, fillO); + int fillColor = CueInfoBuilder.getArgbColorFromCeaColor(fillR, fillG, fillB, fillO); // second byte int borderType = serviceBlockPacket.readBits(2); // only the lower 2 bits of borderType int borderR = serviceBlockPacket.readBits(2); int borderG = serviceBlockPacket.readBits(2); int borderB = serviceBlockPacket.readBits(2); - int borderColor = CueBuilder.getArgbColorFromCeaColor(borderR, borderG, borderB); + int borderColor = CueInfoBuilder.getArgbColorFromCeaColor(borderR, borderG, borderB); // third byte if (serviceBlockPacket.readBit()) { borderType |= 0x04; // set the top bit of the 3-bit borderType @@ -706,12 +706,18 @@ public final class Cea708Decoder extends CeaDecoder { // Note that we don't intend to support display effects serviceBlockPacket.skipBits(8); // effectSpeed(4), effectDirection(2), displayEffect(2) - currentCueBuilder.setWindowAttributes(fillColor, borderColor, wordWrapToggle, borderType, - printDirection, scrollDirection, justification); + currentCueInfoBuilder.setWindowAttributes( + fillColor, + borderColor, + wordWrapToggle, + borderType, + printDirection, + scrollDirection, + justification); } private void handleDefineWindow(int window) { - CueBuilder cueBuilder = cueBuilders[window]; + CueInfoBuilder cueInfoBuilder = cueInfoBuilders[window]; // the DefineWindow command contains 6 bytes of data // first byte @@ -736,24 +742,41 @@ public final class Cea708Decoder extends CeaDecoder { int windowStyle = serviceBlockPacket.readBits(3); int penStyle = serviceBlockPacket.readBits(3); - cueBuilder.defineWindow(visible, rowLock, columnLock, priority, relativePositioning, - verticalAnchor, horizontalAnchor, rowCount, columnCount, anchorId, windowStyle, penStyle); + cueInfoBuilder.defineWindow( + visible, + rowLock, + columnLock, + priority, + relativePositioning, + verticalAnchor, + horizontalAnchor, + rowCount, + columnCount, + anchorId, + windowStyle, + penStyle); } private List getDisplayCues() { - List displayCues = new ArrayList<>(); + List displayCueInfos = new ArrayList<>(); for (int i = 0; i < NUM_WINDOWS; i++) { - if (!cueBuilders[i].isEmpty() && cueBuilders[i].isVisible()) { - displayCues.add(cueBuilders[i].build()); + if (!cueInfoBuilders[i].isEmpty() && cueInfoBuilders[i].isVisible()) { + displayCueInfos.add(cueInfoBuilders[i].build()); } } - Collections.sort(displayCues); + Collections.sort( + displayCueInfos, + (thisInfo, thatInfo) -> Integer.compare(thisInfo.priority, thatInfo.priority)); + List displayCues = new ArrayList<>(displayCueInfos.size()); + for (int i = 0; i < displayCueInfos.size(); i++) { + displayCues.add(displayCueInfos.get(i).cue); + } return Collections.unmodifiableList(displayCues); } private void resetCueBuilders() { for (int i = 0; i < NUM_WINDOWS; i++) { - cueBuilders[i].reset(); + cueInfoBuilders[i].reset(); } } @@ -774,9 +797,9 @@ public final class Cea708Decoder extends CeaDecoder { } - // TODO: There is a lot of overlap between Cea708Decoder.CueBuilder and Cea608Decoder.CueBuilder - // which could be refactored into a separate class. - private static final class CueBuilder { + // TODO: There is a lot of overlap between Cea708Decoder.CueInfoBuilder and + // Cea608Decoder.CueBuilder which could be refactored into a separate class. + private static final class CueInfoBuilder { private static final int RELATIVE_CUE_SIZE = 99; private static final int VERTICAL_SIZE = 74; @@ -885,7 +908,7 @@ public final class Cea708Decoder extends CeaDecoder { private int backgroundColor; private int row; - public CueBuilder() { + public CueInfoBuilder() { rolledUpCaptions = new ArrayList<>(); captionStringBuilder = new SpannableStringBuilder(); reset(); @@ -1134,7 +1157,7 @@ public final class Cea708Decoder extends CeaDecoder { return new SpannableString(spannableStringBuilder); } - public Cea708Cue build() { + public Cea708CueInfo build() { if (isEmpty()) { // The cue is empty. return null; @@ -1209,8 +1232,17 @@ public final class Cea708Decoder extends CeaDecoder { boolean windowColorSet = (windowFillColor != COLOR_SOLID_BLACK); - return new Cea708Cue(cueString, alignment, line, Cue.LINE_TYPE_FRACTION, verticalAnchorType, - position, horizontalAnchorType, Cue.DIMEN_UNSET, windowColorSet, windowFillColor, + return new Cea708CueInfo( + cueString, + alignment, + line, + Cue.LINE_TYPE_FRACTION, + verticalAnchorType, + position, + horizontalAnchorType, + Cue.DIMEN_UNSET, + windowColorSet, + windowFillColor, priority); } @@ -1249,7 +1281,54 @@ public final class Cea708Decoder extends CeaDecoder { (green > 1 ? 255 : 0), (blue > 1 ? 255 : 0)); } - } + /** A {@link Cue} for CEA-708. */ + private static final class Cea708CueInfo { + + public final Cue cue; + + /** The priority of the cue box. */ + public final int priority; + + /** + * @param text See {@link Cue#text}. + * @param textAlignment See {@link Cue#textAlignment}. + * @param line See {@link Cue#line}. + * @param lineType See {@link Cue#lineType}. + * @param lineAnchor See {@link Cue#lineAnchor}. + * @param position See {@link Cue#position}. + * @param positionAnchor See {@link Cue#positionAnchor}. + * @param size See {@link Cue#size}. + * @param windowColorSet See {@link Cue#windowColorSet}. + * @param windowColor See {@link Cue#windowColor}. + * @param priority See (@link #priority}. + */ + public Cea708CueInfo( + CharSequence text, + Alignment textAlignment, + float line, + @Cue.LineType int lineType, + @AnchorType int lineAnchor, + float position, + @AnchorType int positionAnchor, + float size, + boolean windowColorSet, + int windowColor, + int priority) { + this.cue = + new Cue( + text, + textAlignment, + line, + lineType, + lineAnchor, + position, + positionAnchor, + size, + windowColorSet, + windowColor); + this.priority = priority; + } + } } 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 8b255ac2bd..81ff0fdd65 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 @@ -41,12 +41,12 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { private static final int TYPE_vttc = 0x76747463; private final ParsableByteArray sampleData; - private final WebvttCue.Builder builder; + private final WebvttCueInfo.Builder builder; public Mp4WebvttDecoder() { super("Mp4WebvttDecoder"); sampleData = new ParsableByteArray(); - builder = new WebvttCue.Builder(); + builder = new WebvttCueInfo.Builder(); } @Override @@ -72,8 +72,9 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { return new Mp4WebvttSubtitle(resultingCueList); } - private static Cue parseVttCueBox(ParsableByteArray sampleData, WebvttCue.Builder builder, - int remainingCueBoxBytes) throws SubtitleDecoderException { + private static Cue parseVttCueBox( + ParsableByteArray sampleData, WebvttCueInfo.Builder builder, int remainingCueBoxBytes) + throws SubtitleDecoderException { builder.reset(); while (remainingCueBoxBytes > 0) { if (remainingCueBoxBytes < BOX_HEADER_SIZE) { @@ -95,7 +96,7 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { // Other VTTCueBox children are still not supported and are ignored. } } - return builder.build(); + return builder.build().cue; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueInfo.java similarity index 87% rename from library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java rename to library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueInfo.java index bfa067e322..c57d14ac5c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueInfo.java @@ -27,14 +27,15 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; /** A representation of a WebVTT cue. */ -public final class WebvttCue extends Cue { +public final class WebvttCueInfo { - private static final float DEFAULT_POSITION = 0.5f; + /* package */ static final float DEFAULT_POSITION = 0.5f; + public final Cue cue; public final long startTime; public final long endTime; - private WebvttCue( + private WebvttCueInfo( long startTime, long endTime, CharSequence text, @@ -45,21 +46,12 @@ public final class WebvttCue extends Cue { float position, @Cue.AnchorType int positionAnchor, float width) { - super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width); + this.cue = + new Cue(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width); this.startTime = startTime; this.endTime = endTime; } - /** - * Returns whether or not this cue should be placed in the default position and rolled-up with - * the other "normal" cues. - * - * @return Whether this cue should be placed in the default position. - */ - public boolean isNormalCue() { - return (line == DIMEN_UNSET && position == DEFAULT_POSITION); - } - /** Builder for WebVTT cues. */ @SuppressWarnings("hiding") public static class Builder { @@ -120,10 +112,10 @@ public final class WebvttCue extends Cue { private float line; // Equivalent to WebVTT's snap-to-lines flag: // https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag - @LineType private int lineType; - @AnchorType private int lineAnchor; + @Cue.LineType private int lineType; + @Cue.AnchorType private int lineAnchor; private float position; - @AnchorType private int positionAnchor; + @Cue.AnchorType private int positionAnchor; private float width; // Initialization methods @@ -154,7 +146,7 @@ public final class WebvttCue extends Cue { // Construction methods. - public WebvttCue build() { + public WebvttCueInfo build() { line = computeLine(line, lineType); if (position == Cue.DIMEN_UNSET) { @@ -167,7 +159,7 @@ public final class WebvttCue extends Cue { width = Math.min(width, deriveMaxSize(positionAnchor, position)); - return new WebvttCue( + return new WebvttCueInfo( startTime, endTime, Assertions.checkNotNull(text), @@ -205,12 +197,12 @@ public final class WebvttCue extends Cue { return this; } - public Builder setLineType(@LineType int lineType) { + public Builder setLineType(@Cue.LineType int lineType) { this.lineType = lineType; return this; } - public Builder setLineAnchor(@AnchorType int lineAnchor) { + public Builder setLineAnchor(@Cue.AnchorType int lineAnchor) { this.lineAnchor = lineAnchor; return this; } @@ -220,7 +212,7 @@ public final class WebvttCue extends Cue { return this; } - public Builder setPositionAnchor(@AnchorType int positionAnchor) { + public Builder setPositionAnchor(@Cue.AnchorType int positionAnchor) { this.positionAnchor = positionAnchor; return this; } @@ -231,7 +223,7 @@ public final class WebvttCue extends Cue { } // https://www.w3.org/TR/webvtt1/#webvtt-cue-line - private static float computeLine(float line, @LineType int lineType) { + 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)) { @@ -242,9 +234,9 @@ public final class WebvttCue extends Cue { } 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 - // and WebvttCue#isNormalCue. - return DIMEN_UNSET; + // Steps 4 - 10 (stacking multiple simultaneous cues) are handled by + // WebvttSubtitle.getCues(long) and WebvttSubtitle.isNormal(Cue). + return Cue.DIMEN_UNSET; } } @@ -264,7 +256,7 @@ public final class WebvttCue extends Cue { } // https://www.w3.org/TR/webvtt1/#webvtt-cue-position-alignment - @AnchorType + @Cue.AnchorType private static int derivePositionAnchor(@TextAlignment int textAlignment) { switch (textAlignment) { case TextAlignment.LEFT: @@ -297,7 +289,7 @@ public final class WebvttCue extends Cue { } // Step 2 here: https://www.w3.org/TR/webvtt1/#processing-cue-settings - private static float deriveMaxSize(@AnchorType int positionAnchor, float position) { + private static float deriveMaxSize(@Cue.AnchorType int positionAnchor, float position) { switch (positionAnchor) { case Cue.ANCHOR_TYPE_START: return 1.0f - position; 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 6e5bd31b4b..e6fa78ca65 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 @@ -91,7 +91,7 @@ public final class WebvttCueParser { * @return Whether a valid Cue was found. */ public boolean parseCue( - ParsableByteArray webvttData, WebvttCue.Builder builder, List styles) { + ParsableByteArray webvttData, WebvttCueInfo.Builder builder, List styles) { @Nullable String firstLine = webvttData.readLine(); if (firstLine == null) { return false; @@ -119,10 +119,10 @@ public final class WebvttCueParser { * Parses a string containing a list of cue settings. * * @param cueSettingsList String containing the settings for a given cue. - * @param builder The {@link WebvttCue.Builder} where incremental construction takes place. + * @param builder The {@link WebvttCueInfo.Builder} where incremental construction takes place. */ - /* package */ static void parseCueSettingsList(String cueSettingsList, - WebvttCue.Builder 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()) { @@ -147,7 +147,8 @@ public final class WebvttCueParser { } /** - * Parses the text payload of a WebVTT Cue and applies modifications on {@link WebvttCue.Builder}. + * Parses the text payload of a WebVTT Cue and applies modifications on {@link + * WebvttCueInfo.Builder}. * * @param id Id of the cue, {@code null} if it is not present. * @param markup The markup text to be parsed. @@ -155,7 +156,10 @@ public final class WebvttCueParser { * @param builder Output builder. */ /* package */ static void parseCueText( - @Nullable String id, String markup, WebvttCue.Builder builder, List styles) { + @Nullable String id, + String markup, + WebvttCueInfo.Builder builder, + List styles) { SpannableStringBuilder spannedText = new SpannableStringBuilder(); ArrayDeque startTagStack = new ArrayDeque<>(); List scratchStyleMatches = new ArrayList<>(); @@ -230,7 +234,7 @@ public final class WebvttCueParser { @Nullable String id, Matcher cueHeaderMatcher, ParsableByteArray webvttData, - WebvttCue.Builder builder, + WebvttCueInfo.Builder builder, StringBuilder textBuilder, List styles) { try { @@ -260,7 +264,7 @@ public final class WebvttCueParser { // Internal methods - private static void parseLineAttribute(String s, WebvttCue.Builder builder) { + private static void parseLineAttribute(String s, WebvttCueInfo.Builder builder) { int commaIndex = s.indexOf(','); if (commaIndex != -1) { builder.setLineAnchor(parsePositionAnchor(s.substring(commaIndex + 1))); @@ -279,7 +283,7 @@ public final class WebvttCueParser { } } - private static void parsePositionAttribute(String s, WebvttCue.Builder builder) { + private static void parsePositionAttribute(String s, WebvttCueInfo.Builder builder) { int commaIndex = s.indexOf(','); if (commaIndex != -1) { builder.setPositionAnchor(parsePositionAnchor(s.substring(commaIndex + 1))); @@ -304,24 +308,24 @@ public final class WebvttCueParser { } } - @WebvttCue.Builder.TextAlignment + @WebvttCueInfo.Builder.TextAlignment private static int parseTextAlignment(String s) { switch (s) { case "start": - return WebvttCue.Builder.TextAlignment.START; + return WebvttCueInfo.Builder.TextAlignment.START; case "left": - return WebvttCue.Builder.TextAlignment.LEFT; + return WebvttCueInfo.Builder.TextAlignment.LEFT; case "center": case "middle": - return WebvttCue.Builder.TextAlignment.CENTER; + return WebvttCueInfo.Builder.TextAlignment.CENTER; case "end": - return WebvttCue.Builder.TextAlignment.END; + return WebvttCueInfo.Builder.TextAlignment.END; case "right": - return WebvttCue.Builder.TextAlignment.RIGHT; + return WebvttCueInfo.Builder.TextAlignment.RIGHT; default: Log.w(TAG, "Invalid alignment value: " + s); // Default value: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment - return WebvttCue.Builder.TextAlignment.CENTER; + return WebvttCueInfo.Builder.TextAlignment.CENTER; } } 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 9b356f0988..6c1d61d126 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 @@ -42,7 +42,7 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { private final WebvttCueParser cueParser; private final ParsableByteArray parsableWebvttData; - private final WebvttCue.Builder webvttCueBuilder; + private final WebvttCueInfo.Builder webvttCueBuilder; private final CssParser cssParser; private final List definedStyles; @@ -50,7 +50,7 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { super("WebvttDecoder"); cueParser = new WebvttCueParser(); parsableWebvttData = new ParsableByteArray(); - webvttCueBuilder = new WebvttCue.Builder(); + webvttCueBuilder = new WebvttCueInfo.Builder(); cssParser = new CssParser(); definedStyles = new ArrayList<>(); } @@ -72,24 +72,24 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {} int event; - ArrayList subtitles = new ArrayList<>(); + List cueInfos = new ArrayList<>(); while ((event = getNextEvent(parsableWebvttData)) != EVENT_END_OF_FILE) { if (event == EVENT_COMMENT) { skipComment(parsableWebvttData); } else if (event == EVENT_STYLE_BLOCK) { - if (!subtitles.isEmpty()) { + if (!cueInfos.isEmpty()) { throw new SubtitleDecoderException("A style block was found after the first cue."); } parsableWebvttData.readLine(); // Consume the "STYLE" header. definedStyles.addAll(cssParser.parseBlock(parsableWebvttData)); } else if (event == EVENT_CUE) { if (cueParser.parseCue(parsableWebvttData, webvttCueBuilder, definedStyles)) { - subtitles.add(webvttCueBuilder.build()); + cueInfos.add(webvttCueBuilder.build()); webvttCueBuilder.reset(); } } } - return new WebvttSubtitle(subtitles); + return new WebvttSubtitle(cueInfos); } /** 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 2833ff2d0b..83c588fb77 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 @@ -30,23 +30,20 @@ import java.util.List; */ /* package */ final class WebvttSubtitle implements Subtitle { - private final List cues; - private final int numCues; + private final List cues; private final long[] cueTimesUs; private final long[] sortedCueTimesUs; - /** - * @param cues A list of the cues in this subtitle. - */ - public WebvttSubtitle(List cues) { - this.cues = cues; - numCues = cues.size(); - cueTimesUs = new long[2 * numCues]; - for (int cueIndex = 0; cueIndex < numCues; cueIndex++) { - WebvttCue cue = cues.get(cueIndex); + /** Constructs a new WebvttSubtitle from a list of {@link WebvttCueInfo}s. */ + public WebvttSubtitle(List cueInfos) { + this.cues = new ArrayList<>(cueInfos.size()); + cueTimesUs = new long[2 * cueInfos.size()]; + for (int cueIndex = 0; cueIndex < cueInfos.size(); cueIndex++) { + WebvttCueInfo cueInfo = cueInfos.get(cueIndex); + this.cues.add(cueInfo.cue); int arrayIndex = cueIndex * 2; - cueTimesUs[arrayIndex] = cue.startTime; - cueTimesUs[arrayIndex + 1] = cue.endTime; + cueTimesUs[arrayIndex] = cueInfo.startTime; + cueTimesUs[arrayIndex + 1] = cueInfo.endTime; } sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length); Arrays.sort(sortedCueTimesUs); @@ -73,16 +70,16 @@ import java.util.List; @Override public List getCues(long timeUs) { List list = new ArrayList<>(); - WebvttCue firstNormalCue = null; + Cue firstNormalCue = null; SpannableStringBuilder normalCueTextBuilder = null; - for (int i = 0; i < numCues; i++) { + for (int i = 0; i < cues.size(); i++) { if ((cueTimesUs[i * 2] <= timeUs) && (timeUs < cueTimesUs[i * 2 + 1])) { - WebvttCue cue = cues.get(i); + Cue cue = cues.get(i); // TODO(ibaker): Replace this with a closer implementation of the WebVTT spec (keeping // individual cues, but tweaking their `line` value): // https://www.w3.org/TR/webvtt1/#cue-computed-line - if (cue.isNormalCue()) { + if (isNormal(cue)) { // 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 @@ -104,7 +101,7 @@ 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 WebvttCue.Builder().setText(normalCueTextBuilder).build()); + list.add(new WebvttCueInfo.Builder().setText(normalCueTextBuilder).build().cue); } else if (firstNormalCue != null) { // there was only a single normal cue, so just add it to the list list.add(firstNormalCue); @@ -112,4 +109,13 @@ import java.util.List; return list; } + /** + * Returns whether or not this cue should be placed in the default position and rolled-up with the + * other "normal" cues. + * + * @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); + } } 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 015ee7e23a..69d7caa832 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 WebvttCue.Builder().setText("Hello World").build(); + Cue expectedCue = new WebvttCueInfo.Builder().setText("Hello World").build().cue; 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 WebvttCue.Builder().setText("Hello World").build(); - Cue secondExpectedCue = new WebvttCue.Builder().setText("Bye Bye").build(); + Cue firstExpectedCue = new WebvttCueInfo.Builder().setText("Hello World").build().cue; + Cue secondExpectedCue = new WebvttCueInfo.Builder().setText("Bye Bye").build().cue; 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 32d2dc2060..3f260ab89a 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 @@ -243,9 +243,9 @@ public final class WebvttCueParserTest { } private static Spanned parseCueText(String string) { - WebvttCue.Builder builder = new WebvttCue.Builder(); + WebvttCueInfo.Builder builder = new WebvttCueInfo.Builder(); WebvttCueParser.parseCueText(null, string, builder, Collections.emptyList()); - return (Spanned) builder.build().text; + return (Spanned) builder.build().cue.text; } private static T[] getSpans(Spanned text, Class 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 71927cbceb..621751db94 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 @@ -41,16 +41,16 @@ public class WebvttSubtitleTest { private static final WebvttSubtitle simpleSubtitle; static { - ArrayList simpleSubtitleCues = new ArrayList<>(); - WebvttCue firstCue = - new WebvttCue.Builder() + ArrayList simpleSubtitleCues = new ArrayList<>(); + WebvttCueInfo firstCue = + new WebvttCueInfo.Builder() .setStartTime(1000000) .setEndTime(2000000) .setText(FIRST_SUBTITLE_STRING) .build(); simpleSubtitleCues.add(firstCue); - WebvttCue secondCue = - new WebvttCue.Builder() + WebvttCueInfo secondCue = + new WebvttCueInfo.Builder() .setStartTime(3000000) .setEndTime(4000000) .setText(SECOND_SUBTITLE_STRING) @@ -62,16 +62,16 @@ public class WebvttSubtitleTest { private static final WebvttSubtitle overlappingSubtitle; static { - ArrayList overlappingSubtitleCues = new ArrayList<>(); - WebvttCue firstCue = - new WebvttCue.Builder() + ArrayList overlappingSubtitleCues = new ArrayList<>(); + WebvttCueInfo firstCue = + new WebvttCueInfo.Builder() .setStartTime(1000000) .setEndTime(3000000) .setText(FIRST_SUBTITLE_STRING) .build(); overlappingSubtitleCues.add(firstCue); - WebvttCue secondCue = - new WebvttCue.Builder() + WebvttCueInfo secondCue = + new WebvttCueInfo.Builder() .setStartTime(2000000) .setEndTime(4000000) .setText(SECOND_SUBTITLE_STRING) @@ -83,16 +83,16 @@ public class WebvttSubtitleTest { private static final WebvttSubtitle nestedSubtitle; static { - ArrayList nestedSubtitleCues = new ArrayList<>(); - WebvttCue firstCue = - new WebvttCue.Builder() + ArrayList nestedSubtitleCues = new ArrayList<>(); + WebvttCueInfo firstCue = + new WebvttCueInfo.Builder() .setStartTime(1000000) .setEndTime(4000000) .setText(FIRST_SUBTITLE_STRING) .build(); nestedSubtitleCues.add(firstCue); - WebvttCue secondCue = - new WebvttCue.Builder() + WebvttCueInfo secondCue = + new WebvttCueInfo.Builder() .setStartTime(2000000) .setEndTime(3000000) .setText(SECOND_SUBTITLE_STRING)