summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea608Decoder.java')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea608Decoder.java1014
1 files changed, 1014 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
new file mode 100644
index 0000000000..320b4f3f07
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
@@ -0,0 +1,1014 @@
+/*
+ * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea;
+
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.text.Layout.Alignment;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Subtitle;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleDecoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A {@link SubtitleDecoder} for CEA-608 (also known as "line 21 captions" and "EIA-608").
+ */
+public final class Cea608Decoder extends CeaDecoder {
+
+ private static final String TAG = "Cea608Decoder";
+
+ private static final int CC_VALID_FLAG = 0x04;
+ private static final int CC_TYPE_FLAG = 0x02;
+ private static final int CC_FIELD_FLAG = 0x01;
+
+ private static final int NTSC_CC_FIELD_1 = 0x00;
+ private static final int NTSC_CC_FIELD_2 = 0x01;
+ private static final int NTSC_CC_CHANNEL_1 = 0x00;
+ private static final int NTSC_CC_CHANNEL_2 = 0x01;
+
+ private static final int CC_MODE_UNKNOWN = 0;
+ private static final int CC_MODE_ROLL_UP = 1;
+ private static final int CC_MODE_POP_ON = 2;
+ private static final int CC_MODE_PAINT_ON = 3;
+
+ private static final int[] ROW_INDICES = new int[] {11, 1, 3, 12, 14, 5, 7, 9};
+ private static final int[] COLUMN_INDICES = new int[] {0, 4, 8, 12, 16, 20, 24, 28};
+
+ private static final int[] STYLE_COLORS =
+ new int[] {
+ Color.WHITE, Color.GREEN, Color.BLUE, Color.CYAN, Color.RED, Color.YELLOW, Color.MAGENTA
+ };
+ private static final int STYLE_ITALICS = 0x07;
+ private static final int STYLE_UNCHANGED = 0x08;
+
+ // The default number of rows to display in roll-up captions mode.
+ private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4;
+
+ // An implied first byte for packets that are only 2 bytes long, consisting of marker bits
+ // (0b11111) + valid bit (0b1) + NTSC field 1 type bits (0b00).
+ private static final byte CC_IMPLICIT_DATA_HEADER = (byte) 0xFC;
+
+ /**
+ * Command initiating pop-on style captioning. Subsequent data should be loaded into a
+ * non-displayed memory and held there until the {@link #CTRL_END_OF_CAPTION} command is received,
+ * at which point the non-displayed memory becomes the displayed memory (and vice versa).
+ */
+ private static final byte CTRL_RESUME_CAPTION_LOADING = 0x20;
+
+ private static final byte CTRL_BACKSPACE = 0x21;
+
+ private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24;
+
+ /**
+ * Command initiating roll-up style captioning, with the maximum of 2 rows displayed
+ * simultaneously.
+ */
+ private static final byte CTRL_ROLL_UP_CAPTIONS_2_ROWS = 0x25;
+ /**
+ * Command initiating roll-up style captioning, with the maximum of 3 rows displayed
+ * simultaneously.
+ */
+ private static final byte CTRL_ROLL_UP_CAPTIONS_3_ROWS = 0x26;
+ /**
+ * Command initiating roll-up style captioning, with the maximum of 4 rows displayed
+ * simultaneously.
+ */
+ private static final byte CTRL_ROLL_UP_CAPTIONS_4_ROWS = 0x27;
+
+ /**
+ * Command initiating paint-on style captioning. Subsequent data should be addressed immediately
+ * to displayed memory without need for the {@link #CTRL_RESUME_CAPTION_LOADING} command.
+ */
+ private static final byte CTRL_RESUME_DIRECT_CAPTIONING = 0x29;
+ /**
+ * TEXT commands are switching to TEXT service. All consecutive incoming data must be filtered out
+ * until a command is received that switches back to the CAPTION service.
+ */
+ private static final byte CTRL_TEXT_RESTART = 0x2A;
+
+ private static final byte CTRL_RESUME_TEXT_DISPLAY = 0x2B;
+
+ private static final byte CTRL_ERASE_DISPLAYED_MEMORY = 0x2C;
+ private static final byte CTRL_CARRIAGE_RETURN = 0x2D;
+ private static final byte CTRL_ERASE_NON_DISPLAYED_MEMORY = 0x2E;
+
+ /**
+ * Command indicating the end of a pop-on style caption. At this point the caption loaded in
+ * non-displayed memory should be swapped with the one in displayed memory. If no {@link
+ * #CTRL_RESUME_CAPTION_LOADING} command has been received, this command forces the receiver into
+ * pop-on style.
+ */
+ private static final byte CTRL_END_OF_CAPTION = 0x2F;
+
+ // Basic North American 608 CC char set, mostly ASCII. Indexed by (char-0x20).
+ private static final int[] BASIC_CHARACTER_SET = new int[] {
+ 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, // ! " # $ % & '
+ 0x28, 0x29, // ( )
+ 0xE1, // 2A: 225 'á' "Latin small letter A with acute"
+ 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, // + , - . /
+ 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 0 1 2 3 4 5 6 7
+ 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, // 8 9 : ; < = > ?
+ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, // @ A B C D E F G
+ 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, // H I J K L M N O
+ 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, // P Q R S T U V W
+ 0x58, 0x59, 0x5A, 0x5B, // X Y Z [
+ 0xE9, // 5C: 233 'é' "Latin small letter E with acute"
+ 0x5D, // ]
+ 0xED, // 5E: 237 'í' "Latin small letter I with acute"
+ 0xF3, // 5F: 243 'ó' "Latin small letter O with acute"
+ 0xFA, // 60: 250 'ú' "Latin small letter U with acute"
+ 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, // a b c d e f g
+ 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, // h i j k l m n o
+ 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, // p q r s t u v w
+ 0x78, 0x79, 0x7A, // x y z
+ 0xE7, // 7B: 231 'ç' "Latin small letter C with cedilla"
+ 0xF7, // 7C: 247 '÷' "Division sign"
+ 0xD1, // 7D: 209 'Ñ' "Latin capital letter N with tilde"
+ 0xF1, // 7E: 241 'ñ' "Latin small letter N with tilde"
+ 0x25A0 // 7F: "Black Square" (NB: 2588 = Full Block)
+ };
+
+ // Special North American 608 CC char set.
+ private static final int[] SPECIAL_CHARACTER_SET = new int[] {
+ 0xAE, // 30: 174 '®' "Registered Sign" - registered trademark symbol
+ 0xB0, // 31: 176 '°' "Degree Sign"
+ 0xBD, // 32: 189 '½' "Vulgar Fraction One Half" (1/2 symbol)
+ 0xBF, // 33: 191 '¿' "Inverted Question Mark"
+ 0x2122, // 34: "Trade Mark Sign" (tm superscript)
+ 0xA2, // 35: 162 '¢' "Cent Sign"
+ 0xA3, // 36: 163 '£' "Pound Sign" - pounds sterling
+ 0x266A, // 37: "Eighth Note" - music note
+ 0xE0, // 38: 224 'à' "Latin small letter A with grave"
+ 0x20, // 39: TRANSPARENT SPACE - for now use ordinary space
+ 0xE8, // 3A: 232 'è' "Latin small letter E with grave"
+ 0xE2, // 3B: 226 'â' "Latin small letter A with circumflex"
+ 0xEA, // 3C: 234 'ê' "Latin small letter E with circumflex"
+ 0xEE, // 3D: 238 'î' "Latin small letter I with circumflex"
+ 0xF4, // 3E: 244 'ô' "Latin small letter O with circumflex"
+ 0xFB // 3F: 251 'û' "Latin small letter U with circumflex"
+ };
+
+ // Extended Spanish/Miscellaneous and French char set.
+ private static final int[] SPECIAL_ES_FR_CHARACTER_SET = new int[] {
+ // Spanish and misc.
+ 0xC1, 0xC9, 0xD3, 0xDA, 0xDC, 0xFC, 0x2018, 0xA1,
+ 0x2A, 0x27, 0x2014, 0xA9, 0x2120, 0x2022, 0x201C, 0x201D,
+ // French.
+ 0xC0, 0xC2, 0xC7, 0xC8, 0xCA, 0xCB, 0xEB, 0xCE,
+ 0xCF, 0xEF, 0xD4, 0xD9, 0xF9, 0xDB, 0xAB, 0xBB
+ };
+
+ //Extended Portuguese and German/Danish char set.
+ private static final int[] SPECIAL_PT_DE_CHARACTER_SET = new int[] {
+ // Portuguese.
+ 0xC3, 0xE3, 0xCD, 0xCC, 0xEC, 0xD2, 0xF2, 0xD5,
+ 0xF5, 0x7B, 0x7D, 0x5C, 0x5E, 0x5F, 0x7C, 0x7E,
+ // German/Danish.
+ 0xC4, 0xE4, 0xD6, 0xF6, 0xDF, 0xA5, 0xA4, 0x2502,
+ 0xC5, 0xE5, 0xD8, 0xF8, 0x250C, 0x2510, 0x2514, 0x2518
+ };
+
+ private static final boolean[] ODD_PARITY_BYTE_TABLE = {
+ false, true, true, false, true, false, false, true, // 0
+ true, false, false, true, false, true, true, false, // 8
+ true, false, false, true, false, true, true, false, // 16
+ false, true, true, false, true, false, false, true, // 24
+ true, false, false, true, false, true, true, false, // 32
+ false, true, true, false, true, false, false, true, // 40
+ false, true, true, false, true, false, false, true, // 48
+ true, false, false, true, false, true, true, false, // 56
+ true, false, false, true, false, true, true, false, // 64
+ false, true, true, false, true, false, false, true, // 72
+ false, true, true, false, true, false, false, true, // 80
+ true, false, false, true, false, true, true, false, // 88
+ false, true, true, false, true, false, false, true, // 96
+ true, false, false, true, false, true, true, false, // 104
+ true, false, false, true, false, true, true, false, // 112
+ false, true, true, false, true, false, false, true, // 120
+ true, false, false, true, false, true, true, false, // 128
+ false, true, true, false, true, false, false, true, // 136
+ false, true, true, false, true, false, false, true, // 144
+ true, false, false, true, false, true, true, false, // 152
+ false, true, true, false, true, false, false, true, // 160
+ true, false, false, true, false, true, true, false, // 168
+ true, false, false, true, false, true, true, false, // 176
+ false, true, true, false, true, false, false, true, // 184
+ false, true, true, false, true, false, false, true, // 192
+ true, false, false, true, false, true, true, false, // 200
+ true, false, false, true, false, true, true, false, // 208
+ false, true, true, false, true, false, false, true, // 216
+ true, false, false, true, false, true, true, false, // 224
+ false, true, true, false, true, false, false, true, // 232
+ false, true, true, false, true, false, false, true, // 240
+ true, false, false, true, false, true, true, false, // 248
+ };
+
+ private final ParsableByteArray ccData;
+ private final int packetLength;
+ private final int selectedField;
+ private final int selectedChannel;
+ private final ArrayList<CueBuilder> cueBuilders;
+
+ private CueBuilder currentCueBuilder;
+ private List<Cue> cues;
+ private List<Cue> lastCues;
+
+ private int captionMode;
+ private int captionRowCount;
+
+ private boolean isCaptionValid;
+ private boolean repeatableControlSet;
+ private byte repeatableControlCc1;
+ private byte repeatableControlCc2;
+ private int currentChannel;
+
+ // The incoming characters may belong to 3 different services based on the last received control
+ // codes. The 3 services are Captioning, Text and XDS. The decoder only processes Captioning
+ // service bytes and drops the rest.
+ private boolean isInCaptionService;
+
+ public Cea608Decoder(String mimeType, int accessibilityChannel) {
+ ccData = new ParsableByteArray();
+ cueBuilders = new ArrayList<>();
+ currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT);
+ currentChannel = NTSC_CC_CHANNEL_1;
+ packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3;
+ switch (accessibilityChannel) {
+ case 1:
+ selectedChannel = NTSC_CC_CHANNEL_1;
+ selectedField = NTSC_CC_FIELD_1;
+ break;
+ case 2:
+ selectedChannel = NTSC_CC_CHANNEL_2;
+ selectedField = NTSC_CC_FIELD_1;
+ break;
+ case 3:
+ selectedChannel = NTSC_CC_CHANNEL_1;
+ selectedField = NTSC_CC_FIELD_2;
+ break;
+ case 4:
+ selectedChannel = NTSC_CC_CHANNEL_2;
+ selectedField = NTSC_CC_FIELD_2;
+ break;
+ default:
+ Log.w(TAG, "Invalid channel. Defaulting to CC1.");
+ selectedChannel = NTSC_CC_CHANNEL_1;
+ selectedField = NTSC_CC_FIELD_1;
+ }
+
+ setCaptionMode(CC_MODE_UNKNOWN);
+ resetCueBuilders();
+ isInCaptionService = true;
+ }
+
+ @Override
+ public String getName() {
+ return "Cea608Decoder";
+ }
+
+ @Override
+ public void flush() {
+ super.flush();
+ cues = null;
+ lastCues = null;
+ setCaptionMode(CC_MODE_UNKNOWN);
+ setCaptionRowCount(DEFAULT_CAPTIONS_ROW_COUNT);
+ resetCueBuilders();
+ isCaptionValid = false;
+ repeatableControlSet = false;
+ repeatableControlCc1 = 0;
+ repeatableControlCc2 = 0;
+ currentChannel = NTSC_CC_CHANNEL_1;
+ isInCaptionService = true;
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ @Override
+ protected boolean isNewSubtitleDataAvailable() {
+ return cues != lastCues;
+ }
+
+ @Override
+ protected Subtitle createSubtitle() {
+ lastCues = cues;
+ return new CeaSubtitle(cues);
+ }
+
+ @SuppressWarnings("ByteBufferBackingArray")
+ @Override
+ protected void decode(SubtitleInputBuffer inputBuffer) {
+ ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit());
+ boolean captionDataProcessed = false;
+ while (ccData.bytesLeft() >= packetLength) {
+ byte ccHeader = packetLength == 2 ? CC_IMPLICIT_DATA_HEADER
+ : (byte) ccData.readUnsignedByte();
+ int ccByte1 = ccData.readUnsignedByte();
+ int ccByte2 = ccData.readUnsignedByte();
+
+ // TODO: We're currently ignoring the top 5 marker bits, which should all be 1s according
+ // to the CEA-608 specification. We need to determine if the data should be handled
+ // differently when that is not the case.
+
+ if ((ccHeader & CC_TYPE_FLAG) != 0) {
+ // Do not process anything that is not part of the 608 byte stream.
+ continue;
+ }
+
+ if ((ccHeader & CC_FIELD_FLAG) != selectedField) {
+ // Do not process packets not within the selected field.
+ continue;
+ }
+
+ // Strip the parity bit from each byte to get CC data.
+ byte ccData1 = (byte) (ccByte1 & 0x7F);
+ byte ccData2 = (byte) (ccByte2 & 0x7F);
+
+ if (ccData1 == 0 && ccData2 == 0) {
+ // Ignore empty captions.
+ continue;
+ }
+
+ boolean previousIsCaptionValid = isCaptionValid;
+ isCaptionValid =
+ (ccHeader & CC_VALID_FLAG) == CC_VALID_FLAG
+ && ODD_PARITY_BYTE_TABLE[ccByte1]
+ && ODD_PARITY_BYTE_TABLE[ccByte2];
+
+ if (isRepeatedCommand(isCaptionValid, ccData1, ccData2)) {
+ // Ignore repeated valid commands.
+ continue;
+ }
+
+ if (!isCaptionValid) {
+ if (previousIsCaptionValid) {
+ // The encoder has flipped the validity bit to indicate captions are being turned off.
+ resetCueBuilders();
+ captionDataProcessed = true;
+ }
+ continue;
+ }
+
+ maybeUpdateIsInCaptionService(ccData1, ccData2);
+ if (!isInCaptionService) {
+ // Only the Captioning service is supported. Drop all other bytes.
+ continue;
+ }
+
+ if (!updateAndVerifyCurrentChannel(ccData1)) {
+ // Wrong channel.
+ continue;
+ }
+
+ if (isCtrlCode(ccData1)) {
+ if (isSpecialNorthAmericanChar(ccData1, ccData2)) {
+ currentCueBuilder.append(getSpecialNorthAmericanChar(ccData2));
+ } else if (isExtendedWestEuropeanChar(ccData1, ccData2)) {
+ // Remove standard equivalent of the special extended char before appending new one.
+ currentCueBuilder.backspace();
+ currentCueBuilder.append(getExtendedWestEuropeanChar(ccData1, ccData2));
+ } else if (isMidrowCtrlCode(ccData1, ccData2)) {
+ handleMidrowCtrl(ccData2);
+ } else if (isPreambleAddressCode(ccData1, ccData2)) {
+ handlePreambleAddressCode(ccData1, ccData2);
+ } else if (isTabCtrlCode(ccData1, ccData2)) {
+ currentCueBuilder.tabOffset = ccData2 - 0x20;
+ } else if (isMiscCode(ccData1, ccData2)) {
+ handleMiscCode(ccData2);
+ }
+ } else {
+ // Basic North American character set.
+ currentCueBuilder.append(getBasicChar(ccData1));
+ if ((ccData2 & 0xE0) != 0x00) {
+ currentCueBuilder.append(getBasicChar(ccData2));
+ }
+ }
+ captionDataProcessed = true;
+ }
+
+ if (captionDataProcessed) {
+ if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
+ cues = getDisplayCues();
+ }
+ }
+ }
+
+ private boolean updateAndVerifyCurrentChannel(byte cc1) {
+ if (isCtrlCode(cc1)) {
+ currentChannel = getChannel(cc1);
+ }
+ return currentChannel == selectedChannel;
+ }
+
+ private boolean isRepeatedCommand(boolean captionValid, byte cc1, byte cc2) {
+ // Most control commands are sent twice in succession to ensure they are received properly. We
+ // don't want to process duplicate commands, so if we see the same repeatable command twice in a
+ // row then we ignore the second one.
+ if (captionValid && isRepeatable(cc1)) {
+ if (repeatableControlSet && repeatableControlCc1 == cc1 && repeatableControlCc2 == cc2) {
+ // This is a repeated command, so we ignore it.
+ repeatableControlSet = false;
+ return true;
+ } else {
+ // This is the first occurrence of a repeatable command. Set the repeatable control
+ // variables so that we can recognize and ignore a duplicate (if there is one), and then
+ // continue to process the command below.
+ repeatableControlSet = true;
+ repeatableControlCc1 = cc1;
+ repeatableControlCc2 = cc2;
+ }
+ } else {
+ // This command is not repeatable.
+ repeatableControlSet = false;
+ }
+ return false;
+ }
+
+ private void handleMidrowCtrl(byte cc2) {
+ // TODO: support the extended styles (i.e. backgrounds and transparencies)
+
+ // A midrow control code advances the cursor.
+ currentCueBuilder.append(' ');
+
+ // cc2 - 0|0|1|0|STYLE|U
+ boolean underline = (cc2 & 0x01) == 0x01;
+ int style = (cc2 >> 1) & 0x07;
+ currentCueBuilder.setStyle(style, underline);
+ }
+
+ private void handlePreambleAddressCode(byte cc1, byte cc2) {
+ // cc1 - 0|0|0|1|C|E|ROW
+ // C is the channel toggle, E is the extended flag, and ROW is the encoded row
+ int row = ROW_INDICES[cc1 & 0x07];
+ // TODO: support the extended address and style
+
+ // cc2 - 0|1|N|ATTRBTE|U
+ // N is the next row down toggle, ATTRBTE is the 4-byte encoded attribute, and U is the
+ // underline toggle.
+ boolean nextRowDown = (cc2 & 0x20) != 0;
+ if (nextRowDown) {
+ row++;
+ }
+
+ if (row != currentCueBuilder.row) {
+ if (captionMode != CC_MODE_ROLL_UP && !currentCueBuilder.isEmpty()) {
+ currentCueBuilder = new CueBuilder(captionMode, captionRowCount);
+ cueBuilders.add(currentCueBuilder);
+ }
+ currentCueBuilder.row = row;
+ }
+
+ // cc2 - 0|1|N|0|STYLE|U
+ // cc2 - 0|1|N|1|CURSR|U
+ boolean isCursor = (cc2 & 0x10) == 0x10;
+ boolean underline = (cc2 & 0x01) == 0x01;
+ int cursorOrStyle = (cc2 >> 1) & 0x07;
+
+ // We need to call setStyle even for the isCursor case, to update the underline bit.
+ // STYLE_UNCHANGED is used for this case.
+ currentCueBuilder.setStyle(isCursor ? STYLE_UNCHANGED : cursorOrStyle, underline);
+
+ if (isCursor) {
+ currentCueBuilder.indent = COLUMN_INDICES[cursorOrStyle];
+ }
+ }
+
+ private void handleMiscCode(byte cc2) {
+ switch (cc2) {
+ case CTRL_ROLL_UP_CAPTIONS_2_ROWS:
+ setCaptionMode(CC_MODE_ROLL_UP);
+ setCaptionRowCount(2);
+ return;
+ case CTRL_ROLL_UP_CAPTIONS_3_ROWS:
+ setCaptionMode(CC_MODE_ROLL_UP);
+ setCaptionRowCount(3);
+ return;
+ case CTRL_ROLL_UP_CAPTIONS_4_ROWS:
+ setCaptionMode(CC_MODE_ROLL_UP);
+ setCaptionRowCount(4);
+ return;
+ case CTRL_RESUME_CAPTION_LOADING:
+ setCaptionMode(CC_MODE_POP_ON);
+ return;
+ case CTRL_RESUME_DIRECT_CAPTIONING:
+ setCaptionMode(CC_MODE_PAINT_ON);
+ return;
+ default:
+ // Fall through.
+ break;
+ }
+
+ if (captionMode == CC_MODE_UNKNOWN) {
+ return;
+ }
+
+ switch (cc2) {
+ case CTRL_ERASE_DISPLAYED_MEMORY:
+ cues = Collections.emptyList();
+ if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
+ resetCueBuilders();
+ }
+ break;
+ case CTRL_ERASE_NON_DISPLAYED_MEMORY:
+ resetCueBuilders();
+ break;
+ case CTRL_END_OF_CAPTION:
+ cues = getDisplayCues();
+ resetCueBuilders();
+ break;
+ case CTRL_CARRIAGE_RETURN:
+ // carriage returns only apply to rollup captions; don't bother if we don't have anything
+ // to add a carriage return to
+ if (captionMode == CC_MODE_ROLL_UP && !currentCueBuilder.isEmpty()) {
+ currentCueBuilder.rollUp();
+ }
+ break;
+ case CTRL_BACKSPACE:
+ currentCueBuilder.backspace();
+ break;
+ case CTRL_DELETE_TO_END_OF_ROW:
+ // TODO: implement
+ break;
+ default:
+ // Fall through.
+ break;
+ }
+ }
+
+ private List<Cue> getDisplayCues() {
+ // CEA-608 does not define middle and end alignment, however content providers artificially
+ // introduce them using whitespace. When each cue is built, we try and infer the alignment based
+ // on the amount of whitespace either side of the text. To avoid consecutive cues being aligned
+ // differently, we force all cues to have the same alignment, with start alignment given
+ // preference, then middle alignment, then end alignment.
+ @Cue.AnchorType int positionAnchor = Cue.ANCHOR_TYPE_END;
+ int cueBuilderCount = cueBuilders.size();
+ List<Cue> cueBuilderCues = new ArrayList<>(cueBuilderCount);
+ for (int i = 0; i < cueBuilderCount; i++) {
+ Cue cue = cueBuilders.get(i).build(/* forcedPositionAnchor= */ Cue.TYPE_UNSET);
+ cueBuilderCues.add(cue);
+ if (cue != null) {
+ positionAnchor = Math.min(positionAnchor, cue.positionAnchor);
+ }
+ }
+
+ // Skip null cues and rebuild any that don't have the preferred alignment.
+ List<Cue> displayCues = new ArrayList<>(cueBuilderCount);
+ for (int i = 0; i < cueBuilderCount; i++) {
+ Cue cue = cueBuilderCues.get(i);
+ if (cue != null) {
+ if (cue.positionAnchor != positionAnchor) {
+ cue = cueBuilders.get(i).build(positionAnchor);
+ }
+ displayCues.add(cue);
+ }
+ }
+
+ return displayCues;
+ }
+
+ private void setCaptionMode(int captionMode) {
+ if (this.captionMode == captionMode) {
+ return;
+ }
+
+ int oldCaptionMode = this.captionMode;
+ this.captionMode = captionMode;
+
+ if (captionMode == CC_MODE_PAINT_ON) {
+ // Switching to paint-on mode should have no effect except to select the mode.
+ for (int i = 0; i < cueBuilders.size(); i++) {
+ cueBuilders.get(i).setCaptionMode(captionMode);
+ }
+ return;
+ }
+
+ // Clear the working memory.
+ resetCueBuilders();
+ if (oldCaptionMode == CC_MODE_PAINT_ON || captionMode == CC_MODE_ROLL_UP
+ || captionMode == CC_MODE_UNKNOWN) {
+ // When switching from paint-on or to roll-up or unknown, we also need to clear the caption.
+ cues = Collections.emptyList();
+ }
+ }
+
+ private void setCaptionRowCount(int captionRowCount) {
+ this.captionRowCount = captionRowCount;
+ currentCueBuilder.setCaptionRowCount(captionRowCount);
+ }
+
+ private void resetCueBuilders() {
+ currentCueBuilder.reset(captionMode);
+ cueBuilders.clear();
+ cueBuilders.add(currentCueBuilder);
+ }
+
+ private void maybeUpdateIsInCaptionService(byte cc1, byte cc2) {
+ if (isXdsControlCode(cc1)) {
+ isInCaptionService = false;
+ } else if (isServiceSwitchCommand(cc1)) {
+ switch (cc2) {
+ case CTRL_TEXT_RESTART:
+ case CTRL_RESUME_TEXT_DISPLAY:
+ isInCaptionService = false;
+ break;
+ case CTRL_END_OF_CAPTION:
+ case CTRL_RESUME_CAPTION_LOADING:
+ case CTRL_RESUME_DIRECT_CAPTIONING:
+ case CTRL_ROLL_UP_CAPTIONS_2_ROWS:
+ case CTRL_ROLL_UP_CAPTIONS_3_ROWS:
+ case CTRL_ROLL_UP_CAPTIONS_4_ROWS:
+ isInCaptionService = true;
+ break;
+ default:
+ // No update.
+ }
+ }
+ }
+
+ private static char getBasicChar(byte ccData) {
+ int index = (ccData & 0x7F) - 0x20;
+ return (char) BASIC_CHARACTER_SET[index];
+ }
+
+ private static boolean isSpecialNorthAmericanChar(byte cc1, byte cc2) {
+ // cc1 - 0|0|0|1|C|0|0|1
+ // cc2 - 0|0|1|1|X|X|X|X
+ return ((cc1 & 0xF7) == 0x11) && ((cc2 & 0xF0) == 0x30);
+ }
+
+ private static char getSpecialNorthAmericanChar(byte ccData) {
+ int index = ccData & 0x0F;
+ return (char) SPECIAL_CHARACTER_SET[index];
+ }
+
+ private static boolean isExtendedWestEuropeanChar(byte cc1, byte cc2) {
+ // cc1 - 0|0|0|1|C|0|1|S
+ // cc2 - 0|0|1|X|X|X|X|X
+ return ((cc1 & 0xF6) == 0x12) && ((cc2 & 0xE0) == 0x20);
+ }
+
+ private static char getExtendedWestEuropeanChar(byte cc1, byte cc2) {
+ if ((cc1 & 0x01) == 0x00) {
+ // Extended Spanish/Miscellaneous and French character set (S = 0).
+ return getExtendedEsFrChar(cc2);
+ } else {
+ // Extended Portuguese and German/Danish character set (S = 1).
+ return getExtendedPtDeChar(cc2);
+ }
+ }
+
+ private static char getExtendedEsFrChar(byte ccData) {
+ int index = ccData & 0x1F;
+ return (char) SPECIAL_ES_FR_CHARACTER_SET[index];
+ }
+
+ private static char getExtendedPtDeChar(byte ccData) {
+ int index = ccData & 0x1F;
+ return (char) SPECIAL_PT_DE_CHARACTER_SET[index];
+ }
+
+ private static boolean isCtrlCode(byte cc1) {
+ // cc1 - 0|0|0|X|X|X|X|X
+ return (cc1 & 0xE0) == 0x00;
+ }
+
+ private static int getChannel(byte cc1) {
+ // cc1 - X|X|X|X|C|X|X|X
+ return (cc1 >> 3) & 0x1;
+ }
+
+ private static boolean isMidrowCtrlCode(byte cc1, byte cc2) {
+ // cc1 - 0|0|0|1|C|0|0|1
+ // cc2 - 0|0|1|0|X|X|X|X
+ return ((cc1 & 0xF7) == 0x11) && ((cc2 & 0xF0) == 0x20);
+ }
+
+ private static boolean isPreambleAddressCode(byte cc1, byte cc2) {
+ // cc1 - 0|0|0|1|C|X|X|X
+ // cc2 - 0|1|X|X|X|X|X|X
+ return ((cc1 & 0xF0) == 0x10) && ((cc2 & 0xC0) == 0x40);
+ }
+
+ private static boolean isTabCtrlCode(byte cc1, byte cc2) {
+ // cc1 - 0|0|0|1|C|1|1|1
+ // cc2 - 0|0|1|0|0|0|0|1 to 0|0|1|0|0|0|1|1
+ return ((cc1 & 0xF7) == 0x17) && (cc2 >= 0x21 && cc2 <= 0x23);
+ }
+
+ private static boolean isMiscCode(byte cc1, byte cc2) {
+ // cc1 - 0|0|0|1|C|1|0|F
+ // cc2 - 0|0|1|0|X|X|X|X
+ return ((cc1 & 0xF6) == 0x14) && ((cc2 & 0xF0) == 0x20);
+ }
+
+ private static boolean isRepeatable(byte cc1) {
+ // cc1 - 0|0|0|1|X|X|X|X
+ return (cc1 & 0xF0) == 0x10;
+ }
+
+ private static boolean isXdsControlCode(byte cc1) {
+ return 0x01 <= cc1 && cc1 <= 0x0F;
+ }
+
+ private static boolean isServiceSwitchCommand(byte cc1) {
+ // cc1 - 0|0|0|1|C|1|0|0
+ return (cc1 & 0xF7) == 0x14;
+ }
+
+ private static class CueBuilder {
+
+ // 608 captions define a 15 row by 32 column screen grid. These constants convert from 608
+ // positions to normalized screen position.
+ private static final int SCREEN_CHARWIDTH = 32;
+ private static final int BASE_ROW = 15;
+
+ private final List<CueStyle> cueStyles;
+ private final List<SpannableString> rolledUpCaptions;
+ private final StringBuilder captionStringBuilder;
+
+ private int row;
+ private int indent;
+ private int tabOffset;
+ private int captionMode;
+ private int captionRowCount;
+
+ public CueBuilder(int captionMode, int captionRowCount) {
+ cueStyles = new ArrayList<>();
+ rolledUpCaptions = new ArrayList<>();
+ captionStringBuilder = new StringBuilder();
+ reset(captionMode);
+ setCaptionRowCount(captionRowCount);
+ }
+
+ public void reset(int captionMode) {
+ this.captionMode = captionMode;
+ cueStyles.clear();
+ rolledUpCaptions.clear();
+ captionStringBuilder.setLength(0);
+ row = BASE_ROW;
+ indent = 0;
+ tabOffset = 0;
+ }
+
+ public boolean isEmpty() {
+ return cueStyles.isEmpty()
+ && rolledUpCaptions.isEmpty()
+ && captionStringBuilder.length() == 0;
+ }
+
+ public void setCaptionMode(int captionMode) {
+ this.captionMode = captionMode;
+ }
+
+ public void setCaptionRowCount(int captionRowCount) {
+ this.captionRowCount = captionRowCount;
+ }
+
+ public void setStyle(int style, boolean underline) {
+ cueStyles.add(new CueStyle(style, underline, captionStringBuilder.length()));
+ }
+
+ public void backspace() {
+ int length = captionStringBuilder.length();
+ if (length > 0) {
+ captionStringBuilder.delete(length - 1, length);
+ // Decrement style start positions if necessary.
+ for (int i = cueStyles.size() - 1; i >= 0; i--) {
+ CueStyle style = cueStyles.get(i);
+ if (style.start == length) {
+ style.start--;
+ } else {
+ // All earlier cues must have style.start < length.
+ break;
+ }
+ }
+ }
+ }
+
+ public void append(char text) {
+ captionStringBuilder.append(text);
+ }
+
+ public void rollUp() {
+ rolledUpCaptions.add(buildCurrentLine());
+ captionStringBuilder.setLength(0);
+ cueStyles.clear();
+ int numRows = Math.min(captionRowCount, row);
+ while (rolledUpCaptions.size() >= numRows) {
+ rolledUpCaptions.remove(0);
+ }
+ }
+
+ public Cue build(@Cue.AnchorType int forcedPositionAnchor) {
+ SpannableStringBuilder cueString = new SpannableStringBuilder();
+ // Add any rolled up captions, separated by new lines.
+ for (int i = 0; i < rolledUpCaptions.size(); i++) {
+ cueString.append(rolledUpCaptions.get(i));
+ cueString.append('\n');
+ }
+ // Add the current line.
+ cueString.append(buildCurrentLine());
+
+ if (cueString.length() == 0) {
+ // The cue is empty.
+ return null;
+ }
+
+ int positionAnchor;
+ // The number of empty columns before the start of the text, in the range [0-31].
+ int startPadding = indent + tabOffset;
+ // The number of empty columns after the end of the text, in the same range.
+ int endPadding = SCREEN_CHARWIDTH - startPadding - cueString.length();
+ int startEndPaddingDelta = startPadding - endPadding;
+ if (forcedPositionAnchor != Cue.TYPE_UNSET) {
+ positionAnchor = forcedPositionAnchor;
+ } else if (captionMode == CC_MODE_POP_ON
+ && (Math.abs(startEndPaddingDelta) < 3 || endPadding < 0)) {
+ // Treat approximately centered pop-on captions as middle aligned. We also treat captions
+ // that are wider than they should be in this way. See
+ // https://github.com/google/ExoPlayer/issues/3534.
+ positionAnchor = Cue.ANCHOR_TYPE_MIDDLE;
+ } else if (captionMode == CC_MODE_POP_ON && startEndPaddingDelta > 0) {
+ // Treat pop-on captions with less padding at the end than the start as end aligned.
+ positionAnchor = Cue.ANCHOR_TYPE_END;
+ } else {
+ // For all other cases assume start aligned.
+ positionAnchor = Cue.ANCHOR_TYPE_START;
+ }
+
+ float position;
+ switch (positionAnchor) {
+ case Cue.ANCHOR_TYPE_MIDDLE:
+ position = 0.5f;
+ break;
+ case Cue.ANCHOR_TYPE_END:
+ position = (float) (SCREEN_CHARWIDTH - endPadding) / SCREEN_CHARWIDTH;
+ // Adjust the position to fit within the safe area.
+ position = position * 0.8f + 0.1f;
+ break;
+ case Cue.ANCHOR_TYPE_START:
+ default:
+ position = (float) startPadding / SCREEN_CHARWIDTH;
+ // Adjust the position to fit within the safe area.
+ position = position * 0.8f + 0.1f;
+ break;
+ }
+
+ int lineAnchor;
+ int line;
+ // Note: Row indices are in the range [1-15].
+ if (captionMode == CC_MODE_ROLL_UP || row > (BASE_ROW / 2)) {
+ lineAnchor = Cue.ANCHOR_TYPE_END;
+ line = row - BASE_ROW;
+ // Two line adjustments. The first is because line indices from the bottom of the window
+ // start from -1 rather than 0. The second is a blank row to act as the safe area.
+ line -= 2;
+ } else {
+ lineAnchor = Cue.ANCHOR_TYPE_START;
+ // Line indices from the top of the window start from 0, but we want a blank row to act as
+ // the safe area. As a result no adjustment is necessary.
+ line = row;
+ }
+
+ return new Cue(
+ cueString,
+ Alignment.ALIGN_NORMAL,
+ line,
+ Cue.LINE_TYPE_NUMBER,
+ lineAnchor,
+ position,
+ positionAnchor,
+ Cue.DIMEN_UNSET);
+ }
+
+ private SpannableString buildCurrentLine() {
+ SpannableStringBuilder builder = new SpannableStringBuilder(captionStringBuilder);
+ int length = builder.length();
+
+ int underlineStartPosition = C.INDEX_UNSET;
+ int italicStartPosition = C.INDEX_UNSET;
+ int colorStartPosition = 0;
+ int color = Color.WHITE;
+
+ boolean nextItalic = false;
+ int nextColor = Color.WHITE;
+
+ for (int i = 0; i < cueStyles.size(); i++) {
+ CueStyle cueStyle = cueStyles.get(i);
+ boolean underline = cueStyle.underline;
+ int style = cueStyle.style;
+ if (style != STYLE_UNCHANGED) {
+ // If the style is a color then italic is cleared.
+ nextItalic = style == STYLE_ITALICS;
+ // If the style is italic then the color is left unchanged.
+ nextColor = style == STYLE_ITALICS ? nextColor : STYLE_COLORS[style];
+ }
+
+ int position = cueStyle.start;
+ int nextPosition = (i + 1) < cueStyles.size() ? cueStyles.get(i + 1).start : length;
+ if (position == nextPosition) {
+ // There are more cueStyles to process at the current position.
+ continue;
+ }
+
+ // Process changes to underline up to the current position.
+ if (underlineStartPosition != C.INDEX_UNSET && !underline) {
+ setUnderlineSpan(builder, underlineStartPosition, position);
+ underlineStartPosition = C.INDEX_UNSET;
+ } else if (underlineStartPosition == C.INDEX_UNSET && underline) {
+ underlineStartPosition = position;
+ }
+ // Process changes to italic up to the current position.
+ if (italicStartPosition != C.INDEX_UNSET && !nextItalic) {
+ setItalicSpan(builder, italicStartPosition, position);
+ italicStartPosition = C.INDEX_UNSET;
+ } else if (italicStartPosition == C.INDEX_UNSET && nextItalic) {
+ italicStartPosition = position;
+ }
+ // Process changes to color up to the current position.
+ if (nextColor != color) {
+ setColorSpan(builder, colorStartPosition, position, color);
+ color = nextColor;
+ colorStartPosition = position;
+ }
+ }
+
+ // Add any final spans.
+ if (underlineStartPosition != C.INDEX_UNSET && underlineStartPosition != length) {
+ setUnderlineSpan(builder, underlineStartPosition, length);
+ }
+ if (italicStartPosition != C.INDEX_UNSET && italicStartPosition != length) {
+ setItalicSpan(builder, italicStartPosition, length);
+ }
+ if (colorStartPosition != length) {
+ setColorSpan(builder, colorStartPosition, length, color);
+ }
+
+ return new SpannableString(builder);
+ }
+
+ private static void setUnderlineSpan(SpannableStringBuilder builder, int start, int end) {
+ builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private static void setItalicSpan(SpannableStringBuilder builder, int start, int end) {
+ builder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private static void setColorSpan(
+ SpannableStringBuilder builder, int start, int end, int color) {
+ if (color == Color.WHITE) {
+ // White is treated as the default color (i.e. no span is attached).
+ return;
+ }
+ builder.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private static class CueStyle {
+
+ public final int style;
+ public final boolean underline;
+
+ public int start;
+
+ public CueStyle(int style, boolean underline, int start) {
+ this.style = style;
+ this.underline = underline;
+ this.start = start;
+ }
+
+ }
+
+ }
+
+}