summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea608Decoder.java1014
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Cue.java62
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Decoder.java1255
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java54
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaDecoder.java204
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaSubtitle.java60
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaUtil.java138
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/package-info.java19
8 files changed, 2806 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;
+ }
+
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Cue.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Cue.java
new file mode 100644
index 0000000000..268b6baec0
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Cue.java
@@ -0,0 +1,62 @@
+/*
+ * 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.text.Layout.Alignment;
+import androidx.annotation.NonNull;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue;
+
+/**
+ * A {@link Cue} for CEA-708.
+ */
+/* package */ final class Cea708Cue extends Cue implements Comparable<Cea708Cue> {
+
+ /**
+ * 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/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Decoder.java
new file mode 100644
index 0000000000..c8af0ed350
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Decoder.java
@@ -0,0 +1,1255 @@
+/*
+ * 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.BackgroundColorSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue.AnchorType;
+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.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray;
+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-708 (also known as "EIA-708").
+ */
+public final class Cea708Decoder extends CeaDecoder {
+
+ private static final String TAG = "Cea708Decoder";
+
+ private static final int NUM_WINDOWS = 8;
+
+ private static final int DTVCC_PACKET_DATA = 0x02;
+ private static final int DTVCC_PACKET_START = 0x03;
+ private static final int CC_VALID_FLAG = 0x04;
+
+ // Base Commands
+ private static final int GROUP_C0_END = 0x1F; // Miscellaneous Control Codes
+ private static final int GROUP_G0_END = 0x7F; // ASCII Printable Characters
+ private static final int GROUP_C1_END = 0x9F; // Captioning Command Control Codes
+ private static final int GROUP_G1_END = 0xFF; // ISO 8859-1 LATIN-1 Character Set
+
+ // Extended Commands
+ private static final int GROUP_C2_END = 0x1F; // Extended Control Code Set 1
+ private static final int GROUP_G2_END = 0x7F; // Extended Miscellaneous Characters
+ private static final int GROUP_C3_END = 0x9F; // Extended Control Code Set 2
+ private static final int GROUP_G3_END = 0xFF; // Future Expansion
+
+ // Group C0 Commands
+ private static final int COMMAND_NUL = 0x00; // Nul
+ private static final int COMMAND_ETX = 0x03; // EndOfText
+ private static final int COMMAND_BS = 0x08; // Backspace
+ private static final int COMMAND_FF = 0x0C; // FormFeed (Flush)
+ private static final int COMMAND_CR = 0x0D; // CarriageReturn
+ private static final int COMMAND_HCR = 0x0E; // ClearLine
+ private static final int COMMAND_EXT1 = 0x10; // Extended Control Code Flag
+ private static final int COMMAND_EXT1_START = 0x11;
+ private static final int COMMAND_EXT1_END = 0x17;
+ private static final int COMMAND_P16_START = 0x18;
+ private static final int COMMAND_P16_END = 0x1F;
+
+ // Group C1 Commands
+ private static final int COMMAND_CW0 = 0x80; // SetCurrentWindow to 0
+ private static final int COMMAND_CW1 = 0x81; // SetCurrentWindow to 1
+ private static final int COMMAND_CW2 = 0x82; // SetCurrentWindow to 2
+ private static final int COMMAND_CW3 = 0x83; // SetCurrentWindow to 3
+ private static final int COMMAND_CW4 = 0x84; // SetCurrentWindow to 4
+ private static final int COMMAND_CW5 = 0x85; // SetCurrentWindow to 5
+ private static final int COMMAND_CW6 = 0x86; // SetCurrentWindow to 6
+ private static final int COMMAND_CW7 = 0x87; // SetCurrentWindow to 7
+ private static final int COMMAND_CLW = 0x88; // ClearWindows (+1 byte)
+ private static final int COMMAND_DSW = 0x89; // DisplayWindows (+1 byte)
+ private static final int COMMAND_HDW = 0x8A; // HideWindows (+1 byte)
+ private static final int COMMAND_TGW = 0x8B; // ToggleWindows (+1 byte)
+ private static final int COMMAND_DLW = 0x8C; // DeleteWindows (+1 byte)
+ private static final int COMMAND_DLY = 0x8D; // Delay (+1 byte)
+ private static final int COMMAND_DLC = 0x8E; // DelayCancel
+ private static final int COMMAND_RST = 0x8F; // Reset
+ private static final int COMMAND_SPA = 0x90; // SetPenAttributes (+2 bytes)
+ private static final int COMMAND_SPC = 0x91; // SetPenColor (+3 bytes)
+ private static final int COMMAND_SPL = 0x92; // SetPenLocation (+2 bytes)
+ private static final int COMMAND_SWA = 0x97; // SetWindowAttributes (+4 bytes)
+ private static final int COMMAND_DF0 = 0x98; // DefineWindow 0 (+6 bytes)
+ private static final int COMMAND_DF1 = 0x99; // DefineWindow 1 (+6 bytes)
+ private static final int COMMAND_DF2 = 0x9A; // DefineWindow 2 (+6 bytes)
+ private static final int COMMAND_DF3 = 0x9B; // DefineWindow 3 (+6 bytes)
+ private static final int COMMAND_DF4 = 0x9C; // DefineWindow 4 (+6 bytes)
+ private static final int COMMAND_DF5 = 0x9D; // DefineWindow 5 (+6 bytes)
+ private static final int COMMAND_DF6 = 0x9E; // DefineWindow 6 (+6 bytes)
+ private static final int COMMAND_DF7 = 0x9F; // DefineWindow 7 (+6 bytes)
+
+ // G0 Table Special Chars
+ private static final int CHARACTER_MN = 0x7F; // MusicNote
+
+ // G2 Table Special Chars
+ private static final int CHARACTER_TSP = 0x20;
+ private static final int CHARACTER_NBTSP = 0x21;
+ private static final int CHARACTER_ELLIPSIS = 0x25;
+ private static final int CHARACTER_BIG_CARONS = 0x2A;
+ private static final int CHARACTER_BIG_OE = 0x2C;
+ private static final int CHARACTER_SOLID_BLOCK = 0x30;
+ private static final int CHARACTER_OPEN_SINGLE_QUOTE = 0x31;
+ private static final int CHARACTER_CLOSE_SINGLE_QUOTE = 0x32;
+ private static final int CHARACTER_OPEN_DOUBLE_QUOTE = 0x33;
+ private static final int CHARACTER_CLOSE_DOUBLE_QUOTE = 0x34;
+ private static final int CHARACTER_BOLD_BULLET = 0x35;
+ private static final int CHARACTER_TM = 0x39;
+ private static final int CHARACTER_SMALL_CARONS = 0x3A;
+ private static final int CHARACTER_SMALL_OE = 0x3C;
+ private static final int CHARACTER_SM = 0x3D;
+ private static final int CHARACTER_DIAERESIS_Y = 0x3F;
+ private static final int CHARACTER_ONE_EIGHTH = 0x76;
+ private static final int CHARACTER_THREE_EIGHTHS = 0x77;
+ private static final int CHARACTER_FIVE_EIGHTHS = 0x78;
+ private static final int CHARACTER_SEVEN_EIGHTHS = 0x79;
+ private static final int CHARACTER_VERTICAL_BORDER = 0x7A;
+ private static final int CHARACTER_UPPER_RIGHT_BORDER = 0x7B;
+ private static final int CHARACTER_LOWER_LEFT_BORDER = 0x7C;
+ private static final int CHARACTER_HORIZONTAL_BORDER = 0x7D;
+ private static final int CHARACTER_LOWER_RIGHT_BORDER = 0x7E;
+ private static final int CHARACTER_UPPER_LEFT_BORDER = 0x7F;
+
+ private final ParsableByteArray ccData;
+ private final ParsableBitArray serviceBlockPacket;
+
+ private final int selectedServiceNumber;
+ private final CueBuilder[] cueBuilders;
+
+ private CueBuilder currentCueBuilder;
+ private List<Cue> cues;
+ private List<Cue> lastCues;
+
+ private DtvCcPacket currentDtvCcPacket;
+ private int currentWindow;
+
+ // TODO: Retrieve isWideAspectRatio from initializationData and use it.
+ public Cea708Decoder(int accessibilityChannel, @Nullable List<byte[]> initializationData) {
+ ccData = new ParsableByteArray();
+ serviceBlockPacket = new ParsableBitArray();
+ selectedServiceNumber = accessibilityChannel == Format.NO_VALUE ? 1 : accessibilityChannel;
+
+ cueBuilders = new CueBuilder[NUM_WINDOWS];
+ for (int i = 0; i < NUM_WINDOWS; i++) {
+ cueBuilders[i] = new CueBuilder();
+ }
+
+ currentCueBuilder = cueBuilders[0];
+ resetCueBuilders();
+ }
+
+ @Override
+ public String getName() {
+ return "Cea708Decoder";
+ }
+
+ @Override
+ public void flush() {
+ super.flush();
+ cues = null;
+ lastCues = null;
+ currentWindow = 0;
+ currentCueBuilder = cueBuilders[currentWindow];
+ resetCueBuilders();
+ currentDtvCcPacket = null;
+ }
+
+ @Override
+ protected boolean isNewSubtitleDataAvailable() {
+ return cues != lastCues;
+ }
+
+ @Override
+ protected Subtitle createSubtitle() {
+ lastCues = cues;
+ return new CeaSubtitle(cues);
+ }
+
+ @Override
+ protected void decode(SubtitleInputBuffer inputBuffer) {
+ // Subtitle input buffers are non-direct and the position is zero, so calling array() is safe.
+ @SuppressWarnings("ByteBufferBackingArray")
+ byte[] inputBufferData = inputBuffer.data.array();
+ ccData.reset(inputBufferData, inputBuffer.data.limit());
+ while (ccData.bytesLeft() >= 3) {
+ int ccTypeAndValid = (ccData.readUnsignedByte() & 0x07);
+
+ int ccType = ccTypeAndValid & (DTVCC_PACKET_DATA | DTVCC_PACKET_START);
+ boolean ccValid = (ccTypeAndValid & CC_VALID_FLAG) == CC_VALID_FLAG;
+ byte ccData1 = (byte) ccData.readUnsignedByte();
+ byte ccData2 = (byte) ccData.readUnsignedByte();
+
+ // Ignore any non-CEA-708 data
+ if (ccType != DTVCC_PACKET_DATA && ccType != DTVCC_PACKET_START) {
+ continue;
+ }
+
+ if (!ccValid) {
+ // This byte-pair isn't valid, ignore it and continue.
+ continue;
+ }
+
+ if (ccType == DTVCC_PACKET_START) {
+ finalizeCurrentPacket();
+
+ int sequenceNumber = (ccData1 & 0xC0) >> 6; // first 2 bits
+ int packetSize = ccData1 & 0x3F; // last 6 bits
+ if (packetSize == 0) {
+ packetSize = 64;
+ }
+
+ currentDtvCcPacket = new DtvCcPacket(sequenceNumber, packetSize);
+ currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2;
+ } else {
+ // The only remaining valid packet type is DTVCC_PACKET_DATA
+ Assertions.checkArgument(ccType == DTVCC_PACKET_DATA);
+
+ if (currentDtvCcPacket == null) {
+ Log.e(TAG, "Encountered DTVCC_PACKET_DATA before DTVCC_PACKET_START");
+ continue;
+ }
+
+ currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData1;
+ currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2;
+ }
+
+ if (currentDtvCcPacket.currentIndex == (currentDtvCcPacket.packetSize * 2 - 1)) {
+ finalizeCurrentPacket();
+ }
+ }
+ }
+
+ private void finalizeCurrentPacket() {
+ if (currentDtvCcPacket == null) {
+ // No packet to finalize;
+ return;
+ }
+
+ processCurrentPacket();
+ currentDtvCcPacket = null;
+ }
+
+ private void processCurrentPacket() {
+ if (currentDtvCcPacket.currentIndex != (currentDtvCcPacket.packetSize * 2 - 1)) {
+ Log.w(TAG, "DtvCcPacket ended prematurely; size is " + (currentDtvCcPacket.packetSize * 2 - 1)
+ + ", but current index is " + currentDtvCcPacket.currentIndex + " (sequence number "
+ + currentDtvCcPacket.sequenceNumber + "); ignoring packet");
+ return;
+ }
+
+ serviceBlockPacket.reset(currentDtvCcPacket.packetData, currentDtvCcPacket.currentIndex);
+
+ int serviceNumber = serviceBlockPacket.readBits(3);
+ int blockSize = serviceBlockPacket.readBits(5);
+ if (serviceNumber == 7) {
+ // extended service numbers
+ serviceBlockPacket.skipBits(2);
+ serviceNumber = serviceBlockPacket.readBits(6);
+ if (serviceNumber < 7) {
+ Log.w(TAG, "Invalid extended service number: " + serviceNumber);
+ }
+ }
+
+ // Ignore packets in which blockSize is 0
+ if (blockSize == 0) {
+ if (serviceNumber != 0) {
+ Log.w(TAG, "serviceNumber is non-zero (" + serviceNumber + ") when blockSize is 0");
+ }
+ return;
+ }
+
+ if (serviceNumber != selectedServiceNumber) {
+ return;
+ }
+
+ // The cues should be updated if we receive a C0 ETX command, any C1 command, or if after
+ // processing the service block any text has been added to the buffer. See CEA-708-B Section
+ // 8.10.4 for more details.
+ boolean cuesNeedUpdate = false;
+
+ while (serviceBlockPacket.bitsLeft() > 0) {
+ int command = serviceBlockPacket.readBits(8);
+ if (command != COMMAND_EXT1) {
+ if (command <= GROUP_C0_END) {
+ handleC0Command(command);
+ // If the C0 command was an ETX command, the cues are updated in handleC0Command.
+ } else if (command <= GROUP_G0_END) {
+ handleG0Character(command);
+ cuesNeedUpdate = true;
+ } else if (command <= GROUP_C1_END) {
+ handleC1Command(command);
+ cuesNeedUpdate = true;
+ } else if (command <= GROUP_G1_END) {
+ handleG1Character(command);
+ cuesNeedUpdate = true;
+ } else {
+ Log.w(TAG, "Invalid base command: " + command);
+ }
+ } else {
+ // Read the extended command
+ command = serviceBlockPacket.readBits(8);
+ if (command <= GROUP_C2_END) {
+ handleC2Command(command);
+ } else if (command <= GROUP_G2_END) {
+ handleG2Character(command);
+ cuesNeedUpdate = true;
+ } else if (command <= GROUP_C3_END) {
+ handleC3Command(command);
+ } else if (command <= GROUP_G3_END) {
+ handleG3Character(command);
+ cuesNeedUpdate = true;
+ } else {
+ Log.w(TAG, "Invalid extended command: " + command);
+ }
+ }
+ }
+
+ if (cuesNeedUpdate) {
+ cues = getDisplayCues();
+ }
+ }
+
+ private void handleC0Command(int command) {
+ switch (command) {
+ case COMMAND_NUL:
+ // Do nothing.
+ break;
+ case COMMAND_ETX:
+ cues = getDisplayCues();
+ break;
+ case COMMAND_BS:
+ currentCueBuilder.backspace();
+ break;
+ case COMMAND_FF:
+ resetCueBuilders();
+ break;
+ case COMMAND_CR:
+ currentCueBuilder.append('\n');
+ break;
+ case COMMAND_HCR:
+ // TODO: Add support for this command.
+ break;
+ default:
+ if (command >= COMMAND_EXT1_START && command <= COMMAND_EXT1_END) {
+ Log.w(TAG, "Currently unsupported COMMAND_EXT1 Command: " + command);
+ serviceBlockPacket.skipBits(8);
+ } else if (command >= COMMAND_P16_START && command <= COMMAND_P16_END) {
+ Log.w(TAG, "Currently unsupported COMMAND_P16 Command: " + command);
+ serviceBlockPacket.skipBits(16);
+ } else {
+ Log.w(TAG, "Invalid C0 command: " + command);
+ }
+ }
+ }
+
+ private void handleC1Command(int command) {
+ int window;
+ switch (command) {
+ case COMMAND_CW0:
+ case COMMAND_CW1:
+ case COMMAND_CW2:
+ case COMMAND_CW3:
+ case COMMAND_CW4:
+ case COMMAND_CW5:
+ case COMMAND_CW6:
+ case COMMAND_CW7:
+ window = (command - COMMAND_CW0);
+ if (currentWindow != window) {
+ currentWindow = window;
+ currentCueBuilder = cueBuilders[window];
+ }
+ break;
+ case COMMAND_CLW:
+ for (int i = 1; i <= NUM_WINDOWS; i++) {
+ if (serviceBlockPacket.readBit()) {
+ cueBuilders[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);
+ }
+ }
+ break;
+ case COMMAND_HDW:
+ for (int i = 1; i <= NUM_WINDOWS; i++) {
+ if (serviceBlockPacket.readBit()) {
+ cueBuilders[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());
+ }
+ }
+ break;
+ case COMMAND_DLW:
+ for (int i = 1; i <= NUM_WINDOWS; i++) {
+ if (serviceBlockPacket.readBit()) {
+ cueBuilders[NUM_WINDOWS - i].reset();
+ }
+ }
+ break;
+ case COMMAND_DLY:
+ // TODO: Add support for delay commands.
+ serviceBlockPacket.skipBits(8);
+ break;
+ case COMMAND_DLC:
+ // TODO: Add support for delay commands.
+ break;
+ case COMMAND_RST:
+ resetCueBuilders();
+ break;
+ case COMMAND_SPA:
+ if (!currentCueBuilder.isDefined()) {
+ // ignore this command if the current window/cue isn't defined
+ serviceBlockPacket.skipBits(16);
+ } else {
+ handleSetPenAttributes();
+ }
+ break;
+ case COMMAND_SPC:
+ if (!currentCueBuilder.isDefined()) {
+ // ignore this command if the current window/cue isn't defined
+ serviceBlockPacket.skipBits(24);
+ } else {
+ handleSetPenColor();
+ }
+ break;
+ case COMMAND_SPL:
+ if (!currentCueBuilder.isDefined()) {
+ // ignore this command if the current window/cue isn't defined
+ serviceBlockPacket.skipBits(16);
+ } else {
+ handleSetPenLocation();
+ }
+ break;
+ case COMMAND_SWA:
+ if (!currentCueBuilder.isDefined()) {
+ // ignore this command if the current window/cue isn't defined
+ serviceBlockPacket.skipBits(32);
+ } else {
+ handleSetWindowAttributes();
+ }
+ break;
+ case COMMAND_DF0:
+ case COMMAND_DF1:
+ case COMMAND_DF2:
+ case COMMAND_DF3:
+ case COMMAND_DF4:
+ case COMMAND_DF5:
+ case COMMAND_DF6:
+ case COMMAND_DF7:
+ window = (command - COMMAND_DF0);
+ handleDefineWindow(window);
+ // We also set the current window to the newly defined window.
+ if (currentWindow != window) {
+ currentWindow = window;
+ currentCueBuilder = cueBuilders[window];
+ }
+ break;
+ default:
+ Log.w(TAG, "Invalid C1 command: " + command);
+ }
+ }
+
+ private void handleC2Command(int command) {
+ // C2 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes
+ if (command <= 0x07) {
+ // Do nothing.
+ } else if (command <= 0x0F) {
+ serviceBlockPacket.skipBits(8);
+ } else if (command <= 0x17) {
+ serviceBlockPacket.skipBits(16);
+ } else if (command <= 0x1F) {
+ serviceBlockPacket.skipBits(24);
+ }
+ }
+
+ private void handleC3Command(int command) {
+ // C3 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes
+ if (command <= 0x87) {
+ serviceBlockPacket.skipBits(32);
+ } else if (command <= 0x8F) {
+ serviceBlockPacket.skipBits(40);
+ } else if (command <= 0x9F) {
+ // 90-9F are variable length codes; the first byte defines the header with the first
+ // 2 bits specifying the type and the last 6 bits specifying the remaining length of the
+ // command in bytes
+ serviceBlockPacket.skipBits(2);
+ int length = serviceBlockPacket.readBits(6);
+ serviceBlockPacket.skipBits(8 * length);
+ }
+ }
+
+ private void handleG0Character(int characterCode) {
+ if (characterCode == CHARACTER_MN) {
+ currentCueBuilder.append('\u266B');
+ } else {
+ currentCueBuilder.append((char) (characterCode & 0xFF));
+ }
+ }
+
+ private void handleG1Character(int characterCode) {
+ currentCueBuilder.append((char) (characterCode & 0xFF));
+ }
+
+ private void handleG2Character(int characterCode) {
+ switch (characterCode) {
+ case CHARACTER_TSP:
+ currentCueBuilder.append('\u0020');
+ break;
+ case CHARACTER_NBTSP:
+ currentCueBuilder.append('\u00A0');
+ break;
+ case CHARACTER_ELLIPSIS:
+ currentCueBuilder.append('\u2026');
+ break;
+ case CHARACTER_BIG_CARONS:
+ currentCueBuilder.append('\u0160');
+ break;
+ case CHARACTER_BIG_OE:
+ currentCueBuilder.append('\u0152');
+ break;
+ case CHARACTER_SOLID_BLOCK:
+ currentCueBuilder.append('\u2588');
+ break;
+ case CHARACTER_OPEN_SINGLE_QUOTE:
+ currentCueBuilder.append('\u2018');
+ break;
+ case CHARACTER_CLOSE_SINGLE_QUOTE:
+ currentCueBuilder.append('\u2019');
+ break;
+ case CHARACTER_OPEN_DOUBLE_QUOTE:
+ currentCueBuilder.append('\u201C');
+ break;
+ case CHARACTER_CLOSE_DOUBLE_QUOTE:
+ currentCueBuilder.append('\u201D');
+ break;
+ case CHARACTER_BOLD_BULLET:
+ currentCueBuilder.append('\u2022');
+ break;
+ case CHARACTER_TM:
+ currentCueBuilder.append('\u2122');
+ break;
+ case CHARACTER_SMALL_CARONS:
+ currentCueBuilder.append('\u0161');
+ break;
+ case CHARACTER_SMALL_OE:
+ currentCueBuilder.append('\u0153');
+ break;
+ case CHARACTER_SM:
+ currentCueBuilder.append('\u2120');
+ break;
+ case CHARACTER_DIAERESIS_Y:
+ currentCueBuilder.append('\u0178');
+ break;
+ case CHARACTER_ONE_EIGHTH:
+ currentCueBuilder.append('\u215B');
+ break;
+ case CHARACTER_THREE_EIGHTHS:
+ currentCueBuilder.append('\u215C');
+ break;
+ case CHARACTER_FIVE_EIGHTHS:
+ currentCueBuilder.append('\u215D');
+ break;
+ case CHARACTER_SEVEN_EIGHTHS:
+ currentCueBuilder.append('\u215E');
+ break;
+ case CHARACTER_VERTICAL_BORDER:
+ currentCueBuilder.append('\u2502');
+ break;
+ case CHARACTER_UPPER_RIGHT_BORDER:
+ currentCueBuilder.append('\u2510');
+ break;
+ case CHARACTER_LOWER_LEFT_BORDER:
+ currentCueBuilder.append('\u2514');
+ break;
+ case CHARACTER_HORIZONTAL_BORDER:
+ currentCueBuilder.append('\u2500');
+ break;
+ case CHARACTER_LOWER_RIGHT_BORDER:
+ currentCueBuilder.append('\u2518');
+ break;
+ case CHARACTER_UPPER_LEFT_BORDER:
+ currentCueBuilder.append('\u250C');
+ break;
+ default:
+ Log.w(TAG, "Invalid G2 character: " + characterCode);
+ // The CEA-708 specification doesn't specify what to do in the case of an unexpected
+ // value in the G2 character range, so we ignore it.
+ }
+ }
+
+ private void handleG3Character(int characterCode) {
+ if (characterCode == 0xA0) {
+ currentCueBuilder.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('_');
+ }
+ }
+
+ private void handleSetPenAttributes() {
+ // the SetPenAttributes command contains 2 bytes of data
+ // first byte
+ int textTag = serviceBlockPacket.readBits(4);
+ int offset = serviceBlockPacket.readBits(2);
+ int penSize = serviceBlockPacket.readBits(2);
+ // second byte
+ boolean italicsToggle = serviceBlockPacket.readBit();
+ boolean underlineToggle = serviceBlockPacket.readBit();
+ int edgeType = serviceBlockPacket.readBits(3);
+ int fontStyle = serviceBlockPacket.readBits(3);
+
+ currentCueBuilder.setPenAttributes(textTag, offset, penSize, italicsToggle, underlineToggle,
+ edgeType, fontStyle);
+ }
+
+ private void handleSetPenColor() {
+ // the SetPenColor command contains 3 bytes of data
+ // first byte
+ int foregroundO = serviceBlockPacket.readBits(2);
+ int foregroundR = serviceBlockPacket.readBits(2);
+ int foregroundG = serviceBlockPacket.readBits(2);
+ int foregroundB = serviceBlockPacket.readBits(2);
+ int foregroundColor = CueBuilder.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);
+ // 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);
+
+ currentCueBuilder.setPenColor(foregroundColor, backgroundColor, edgeColor);
+ }
+
+ private void handleSetPenLocation() {
+ // the SetPenLocation command contains 2 bytes of data
+ // first byte
+ serviceBlockPacket.skipBits(4);
+ int row = serviceBlockPacket.readBits(4);
+ // second byte
+ serviceBlockPacket.skipBits(2);
+ int column = serviceBlockPacket.readBits(6);
+
+ currentCueBuilder.setPenLocation(row, column);
+ }
+
+ private void handleSetWindowAttributes() {
+ // the SetWindowAttributes command contains 4 bytes of data
+ // first byte
+ int fillO = serviceBlockPacket.readBits(2);
+ int fillR = serviceBlockPacket.readBits(2);
+ int fillG = serviceBlockPacket.readBits(2);
+ int fillB = serviceBlockPacket.readBits(2);
+ int fillColor = CueBuilder.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);
+ // third byte
+ if (serviceBlockPacket.readBit()) {
+ borderType |= 0x04; // set the top bit of the 3-bit borderType
+ }
+ boolean wordWrapToggle = serviceBlockPacket.readBit();
+ int printDirection = serviceBlockPacket.readBits(2);
+ int scrollDirection = serviceBlockPacket.readBits(2);
+ int justification = serviceBlockPacket.readBits(2);
+ // fourth byte
+ // 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);
+ }
+
+ private void handleDefineWindow(int window) {
+ CueBuilder cueBuilder = cueBuilders[window];
+
+ // the DefineWindow command contains 6 bytes of data
+ // first byte
+ serviceBlockPacket.skipBits(2); // null padding
+ boolean visible = serviceBlockPacket.readBit();
+ boolean rowLock = serviceBlockPacket.readBit();
+ boolean columnLock = serviceBlockPacket.readBit();
+ int priority = serviceBlockPacket.readBits(3);
+ // second byte
+ boolean relativePositioning = serviceBlockPacket.readBit();
+ int verticalAnchor = serviceBlockPacket.readBits(7);
+ // third byte
+ int horizontalAnchor = serviceBlockPacket.readBits(8);
+ // fourth byte
+ int anchorId = serviceBlockPacket.readBits(4);
+ int rowCount = serviceBlockPacket.readBits(4);
+ // fifth byte
+ serviceBlockPacket.skipBits(2); // null padding
+ int columnCount = serviceBlockPacket.readBits(6);
+ // sixth byte
+ serviceBlockPacket.skipBits(2); // null padding
+ int windowStyle = serviceBlockPacket.readBits(3);
+ int penStyle = serviceBlockPacket.readBits(3);
+
+ cueBuilder.defineWindow(visible, rowLock, columnLock, priority, relativePositioning,
+ verticalAnchor, horizontalAnchor, rowCount, columnCount, anchorId, windowStyle, penStyle);
+ }
+
+ private List<Cue> getDisplayCues() {
+ List<Cea708Cue> displayCues = new ArrayList<>();
+ for (int i = 0; i < NUM_WINDOWS; i++) {
+ if (!cueBuilders[i].isEmpty() && cueBuilders[i].isVisible()) {
+ displayCues.add(cueBuilders[i].build());
+ }
+ }
+ Collections.sort(displayCues);
+ return Collections.unmodifiableList(displayCues);
+ }
+
+ private void resetCueBuilders() {
+ for (int i = 0; i < NUM_WINDOWS; i++) {
+ cueBuilders[i].reset();
+ }
+ }
+
+ private static final class DtvCcPacket {
+
+ public final int sequenceNumber;
+ public final int packetSize;
+ public final byte[] packetData;
+
+ int currentIndex;
+
+ public DtvCcPacket(int sequenceNumber, int packetSize) {
+ this.sequenceNumber = sequenceNumber;
+ this.packetSize = packetSize;
+ packetData = new byte[2 * packetSize - 1];
+ currentIndex = 0;
+ }
+
+ }
+
+ // 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 {
+
+ private static final int RELATIVE_CUE_SIZE = 99;
+ private static final int VERTICAL_SIZE = 74;
+ private static final int HORIZONTAL_SIZE = 209;
+
+ private static final int DEFAULT_PRIORITY = 4;
+
+ private static final int MAXIMUM_ROW_COUNT = 15;
+
+ private static final int JUSTIFICATION_LEFT = 0;
+ private static final int JUSTIFICATION_RIGHT = 1;
+ private static final int JUSTIFICATION_CENTER = 2;
+ private static final int JUSTIFICATION_FULL = 3;
+
+ private static final int DIRECTION_LEFT_TO_RIGHT = 0;
+ private static final int DIRECTION_RIGHT_TO_LEFT = 1;
+ private static final int DIRECTION_TOP_TO_BOTTOM = 2;
+ private static final int DIRECTION_BOTTOM_TO_TOP = 3;
+
+ // TODO: Add other border/edge types when utilized.
+ private static final int BORDER_AND_EDGE_TYPE_NONE = 0;
+ private static final int BORDER_AND_EDGE_TYPE_UNIFORM = 3;
+
+ public static final int COLOR_SOLID_WHITE = getArgbColorFromCeaColor(2, 2, 2, 0);
+ public static final int COLOR_SOLID_BLACK = getArgbColorFromCeaColor(0, 0, 0, 0);
+ public static final int COLOR_TRANSPARENT = getArgbColorFromCeaColor(0, 0, 0, 3);
+
+ // TODO: Add other sizes when utilized.
+ private static final int PEN_SIZE_STANDARD = 1;
+
+ // TODO: Add other pen font styles when utilized.
+ private static final int PEN_FONT_STYLE_DEFAULT = 0;
+ private static final int PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS = 1;
+ private static final int PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS = 2;
+ private static final int PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS = 3;
+ private static final int PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS = 4;
+
+ // TODO: Add other pen offsets when utilized.
+ private static final int PEN_OFFSET_NORMAL = 1;
+
+ // The window style properties are specified in the CEA-708 specification.
+ private static final int[] WINDOW_STYLE_JUSTIFICATION = new int[] {
+ JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT,
+ JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_CENTER,
+ JUSTIFICATION_LEFT
+ };
+ private static final int[] WINDOW_STYLE_PRINT_DIRECTION = new int[] {
+ DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT,
+ DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT,
+ DIRECTION_TOP_TO_BOTTOM
+ };
+ private static final int[] WINDOW_STYLE_SCROLL_DIRECTION = new int[] {
+ DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP,
+ DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP,
+ DIRECTION_RIGHT_TO_LEFT
+ };
+ private static final boolean[] WINDOW_STYLE_WORD_WRAP = new boolean[] {
+ false, false, false, true, true, true, false
+ };
+ private static final int[] WINDOW_STYLE_FILL = new int[] {
+ COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK,
+ COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK
+ };
+
+ // The pen style properties are specified in the CEA-708 specification.
+ private static final int[] PEN_STYLE_FONT_STYLE = new int[] {
+ PEN_FONT_STYLE_DEFAULT, PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS,
+ PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS, PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS,
+ PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS,
+ PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS,
+ PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS
+ };
+ private static final int[] PEN_STYLE_EDGE_TYPE = new int[] {
+ BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE,
+ BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_UNIFORM,
+ BORDER_AND_EDGE_TYPE_UNIFORM
+ };
+ private static final int[] PEN_STYLE_BACKGROUND = new int[] {
+ COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK,
+ COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_TRANSPARENT};
+
+ private final List<SpannableString> rolledUpCaptions;
+ private final SpannableStringBuilder captionStringBuilder;
+
+ // Window/Cue properties
+ private boolean defined;
+ private boolean visible;
+ private int priority;
+ private boolean relativePositioning;
+ private int verticalAnchor;
+ private int horizontalAnchor;
+ private int anchorId;
+ private int rowCount;
+ private boolean rowLock;
+ private int justification;
+ private int windowStyleId;
+ private int penStyleId;
+ private int windowFillColor;
+
+ // Pen/Text properties
+ private int italicsStartPosition;
+ private int underlineStartPosition;
+ private int foregroundColorStartPosition;
+ private int foregroundColor;
+ private int backgroundColorStartPosition;
+ private int backgroundColor;
+ private int row;
+
+ public CueBuilder() {
+ rolledUpCaptions = new ArrayList<>();
+ captionStringBuilder = new SpannableStringBuilder();
+ reset();
+ }
+
+ public boolean isEmpty() {
+ return !isDefined() || (rolledUpCaptions.isEmpty() && captionStringBuilder.length() == 0);
+ }
+
+ public void reset() {
+ clear();
+
+ defined = false;
+ visible = false;
+ priority = DEFAULT_PRIORITY;
+ relativePositioning = false;
+ verticalAnchor = 0;
+ horizontalAnchor = 0;
+ anchorId = 0;
+ rowCount = MAXIMUM_ROW_COUNT;
+ rowLock = true;
+ justification = JUSTIFICATION_LEFT;
+ windowStyleId = 0;
+ penStyleId = 0;
+ windowFillColor = COLOR_SOLID_BLACK;
+
+ foregroundColor = COLOR_SOLID_WHITE;
+ backgroundColor = COLOR_SOLID_BLACK;
+ }
+
+ public void clear() {
+ rolledUpCaptions.clear();
+ captionStringBuilder.clear();
+ italicsStartPosition = C.POSITION_UNSET;
+ underlineStartPosition = C.POSITION_UNSET;
+ foregroundColorStartPosition = C.POSITION_UNSET;
+ backgroundColorStartPosition = C.POSITION_UNSET;
+ row = 0;
+ }
+
+ public boolean isDefined() {
+ return defined;
+ }
+
+ public void setVisibility(boolean visible) {
+ this.visible = visible;
+ }
+
+ public boolean isVisible() {
+ return visible;
+ }
+
+ public void defineWindow(boolean visible, boolean rowLock, boolean columnLock, int priority,
+ boolean relativePositioning, int verticalAnchor, int horizontalAnchor, int rowCount,
+ int columnCount, int anchorId, int windowStyleId, int penStyleId) {
+ this.defined = true;
+ this.visible = visible;
+ this.rowLock = rowLock;
+ this.priority = priority;
+ this.relativePositioning = relativePositioning;
+ this.verticalAnchor = verticalAnchor;
+ this.horizontalAnchor = horizontalAnchor;
+ this.anchorId = anchorId;
+
+ // Decoders must add one to rowCount to get the desired number of rows.
+ if (this.rowCount != rowCount + 1) {
+ this.rowCount = rowCount + 1;
+
+ // Trim any rolled up captions that are no longer valid, if applicable.
+ while ((rowLock && (rolledUpCaptions.size() >= this.rowCount))
+ || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) {
+ rolledUpCaptions.remove(0);
+ }
+ }
+
+ // TODO: Add support for column lock and count.
+
+ if (windowStyleId != 0 && this.windowStyleId != windowStyleId) {
+ this.windowStyleId = windowStyleId;
+ // windowStyleId is 1-based.
+ int windowStyleIdIndex = windowStyleId - 1;
+ // Note that Border type and border color are the same for all window styles.
+ setWindowAttributes(WINDOW_STYLE_FILL[windowStyleIdIndex], COLOR_TRANSPARENT,
+ WINDOW_STYLE_WORD_WRAP[windowStyleIdIndex], BORDER_AND_EDGE_TYPE_NONE,
+ WINDOW_STYLE_PRINT_DIRECTION[windowStyleIdIndex],
+ WINDOW_STYLE_SCROLL_DIRECTION[windowStyleIdIndex],
+ WINDOW_STYLE_JUSTIFICATION[windowStyleIdIndex]);
+ }
+
+ if (penStyleId != 0 && this.penStyleId != penStyleId) {
+ this.penStyleId = penStyleId;
+ // penStyleId is 1-based.
+ int penStyleIdIndex = penStyleId - 1;
+ // Note that pen size, offset, italics, underline, foreground color, and foreground
+ // opacity are the same for all pen styles.
+ setPenAttributes(0, PEN_OFFSET_NORMAL, PEN_SIZE_STANDARD, false, false,
+ PEN_STYLE_EDGE_TYPE[penStyleIdIndex], PEN_STYLE_FONT_STYLE[penStyleIdIndex]);
+ setPenColor(COLOR_SOLID_WHITE, PEN_STYLE_BACKGROUND[penStyleIdIndex], COLOR_SOLID_BLACK);
+ }
+ }
+
+
+ public void setWindowAttributes(int fillColor, int borderColor, boolean wordWrapToggle,
+ int borderType, int printDirection, int scrollDirection, int justification) {
+ this.windowFillColor = fillColor;
+ // TODO: Add support for border color and types.
+ // TODO: Add support for word wrap.
+ // TODO: Add support for other scroll directions.
+ // TODO: Add support for other print directions.
+ this.justification = justification;
+
+ }
+
+ public void setPenAttributes(int textTag, int offset, int penSize, boolean italicsToggle,
+ boolean underlineToggle, int edgeType, int fontStyle) {
+ // TODO: Add support for text tags.
+ // TODO: Add support for other offsets.
+ // TODO: Add support for other pen sizes.
+
+ if (italicsStartPosition != C.POSITION_UNSET) {
+ if (!italicsToggle) {
+ captionStringBuilder.setSpan(new StyleSpan(Typeface.ITALIC), italicsStartPosition,
+ captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ italicsStartPosition = C.POSITION_UNSET;
+ }
+ } else if (italicsToggle) {
+ italicsStartPosition = captionStringBuilder.length();
+ }
+
+ if (underlineStartPosition != C.POSITION_UNSET) {
+ if (!underlineToggle) {
+ captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition,
+ captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ underlineStartPosition = C.POSITION_UNSET;
+ }
+ } else if (underlineToggle) {
+ underlineStartPosition = captionStringBuilder.length();
+ }
+
+ // TODO: Add support for edge types.
+ // TODO: Add support for other font styles.
+ }
+
+ public void setPenColor(int foregroundColor, int backgroundColor, int edgeColor) {
+ if (foregroundColorStartPosition != C.POSITION_UNSET) {
+ if (this.foregroundColor != foregroundColor) {
+ captionStringBuilder.setSpan(new ForegroundColorSpan(this.foregroundColor),
+ foregroundColorStartPosition, captionStringBuilder.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ if (foregroundColor != COLOR_SOLID_WHITE) {
+ foregroundColorStartPosition = captionStringBuilder.length();
+ this.foregroundColor = foregroundColor;
+ }
+
+ if (backgroundColorStartPosition != C.POSITION_UNSET) {
+ if (this.backgroundColor != backgroundColor) {
+ captionStringBuilder.setSpan(new BackgroundColorSpan(this.backgroundColor),
+ backgroundColorStartPosition, captionStringBuilder.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ if (backgroundColor != COLOR_SOLID_BLACK) {
+ backgroundColorStartPosition = captionStringBuilder.length();
+ this.backgroundColor = backgroundColor;
+ }
+
+ // TODO: Add support for edge color.
+ }
+
+ public void setPenLocation(int row, int column) {
+ // TODO: Support moving the pen location with a window properly.
+
+ // Until we support proper pen locations, if we encounter a row that's different from the
+ // previous one, we should append a new line. Otherwise, we'll see strings that should be
+ // on new lines concatenated with the previous, resulting in 2 words being combined, as
+ // well as potentially drawing beyond the width of the window/screen.
+ if (this.row != row) {
+ append('\n');
+ }
+ this.row = row;
+ }
+
+ public void backspace() {
+ int length = captionStringBuilder.length();
+ if (length > 0) {
+ captionStringBuilder.delete(length - 1, length);
+ }
+ }
+
+ public void append(char text) {
+ if (text == '\n') {
+ rolledUpCaptions.add(buildSpannableString());
+ captionStringBuilder.clear();
+
+ if (italicsStartPosition != C.POSITION_UNSET) {
+ italicsStartPosition = 0;
+ }
+ if (underlineStartPosition != C.POSITION_UNSET) {
+ underlineStartPosition = 0;
+ }
+ if (foregroundColorStartPosition != C.POSITION_UNSET) {
+ foregroundColorStartPosition = 0;
+ }
+ if (backgroundColorStartPosition != C.POSITION_UNSET) {
+ backgroundColorStartPosition = 0;
+ }
+
+ while ((rowLock && (rolledUpCaptions.size() >= rowCount))
+ || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) {
+ rolledUpCaptions.remove(0);
+ }
+ } else {
+ captionStringBuilder.append(text);
+ }
+ }
+
+ public SpannableString buildSpannableString() {
+ SpannableStringBuilder spannableStringBuilder =
+ new SpannableStringBuilder(captionStringBuilder);
+ int length = spannableStringBuilder.length();
+
+ if (length > 0) {
+ if (italicsStartPosition != C.POSITION_UNSET) {
+ spannableStringBuilder.setSpan(new StyleSpan(Typeface.ITALIC), italicsStartPosition,
+ length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ if (underlineStartPosition != C.POSITION_UNSET) {
+ spannableStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition,
+ length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ if (foregroundColorStartPosition != C.POSITION_UNSET) {
+ spannableStringBuilder.setSpan(new ForegroundColorSpan(foregroundColor),
+ foregroundColorStartPosition, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ if (backgroundColorStartPosition != C.POSITION_UNSET) {
+ spannableStringBuilder.setSpan(new BackgroundColorSpan(backgroundColor),
+ backgroundColorStartPosition, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ return new SpannableString(spannableStringBuilder);
+ }
+
+ public Cea708Cue build() {
+ if (isEmpty()) {
+ // The cue is empty.
+ return null;
+ }
+
+ 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(buildSpannableString());
+
+ // TODO: Add support for right-to-left languages (i.e. where right would correspond to normal
+ // alignment).
+ Alignment alignment;
+ switch (justification) {
+ case JUSTIFICATION_FULL:
+ // TODO: Add support for full justification.
+ case JUSTIFICATION_LEFT:
+ alignment = Alignment.ALIGN_NORMAL;
+ break;
+ case JUSTIFICATION_RIGHT:
+ alignment = Alignment.ALIGN_OPPOSITE;
+ break;
+ case JUSTIFICATION_CENTER:
+ alignment = Alignment.ALIGN_CENTER;
+ break;
+ default:
+ throw new IllegalArgumentException("Unexpected justification value: " + justification);
+ }
+
+ float position;
+ float line;
+ if (relativePositioning) {
+ position = (float) horizontalAnchor / RELATIVE_CUE_SIZE;
+ line = (float) verticalAnchor / RELATIVE_CUE_SIZE;
+ } else {
+ position = (float) horizontalAnchor / HORIZONTAL_SIZE;
+ line = (float) verticalAnchor / VERTICAL_SIZE;
+ }
+ // Apply screen-edge padding to the line and position.
+ position = (position * 0.9f) + 0.05f;
+ line = (line * 0.9f) + 0.05f;
+
+ // anchorId specifies where the anchor should be placed on the caption cue/window. The 9
+ // possible configurations are as follows:
+ // 0-----1-----2
+ // | |
+ // 3 4 5
+ // | |
+ // 6-----7-----8
+ @AnchorType int verticalAnchorType;
+ if (anchorId % 3 == 0) {
+ verticalAnchorType = Cue.ANCHOR_TYPE_START;
+ } else if (anchorId % 3 == 1) {
+ verticalAnchorType = Cue.ANCHOR_TYPE_MIDDLE;
+ } else {
+ verticalAnchorType = Cue.ANCHOR_TYPE_END;
+ }
+ // TODO: Add support for right-to-left languages (i.e. where start is on the right).
+ @AnchorType int horizontalAnchorType;
+ if (anchorId / 3 == 0) {
+ horizontalAnchorType = Cue.ANCHOR_TYPE_START;
+ } else if (anchorId / 3 == 1) {
+ horizontalAnchorType = Cue.ANCHOR_TYPE_MIDDLE;
+ } else {
+ horizontalAnchorType = Cue.ANCHOR_TYPE_END;
+ }
+
+ boolean windowColorSet = (windowFillColor != COLOR_SOLID_BLACK);
+
+ return new Cea708Cue(cueString, alignment, line, Cue.LINE_TYPE_FRACTION, verticalAnchorType,
+ position, horizontalAnchorType, Cue.DIMEN_UNSET, windowColorSet, windowFillColor,
+ priority);
+ }
+
+ public static int getArgbColorFromCeaColor(int red, int green, int blue) {
+ return getArgbColorFromCeaColor(red, green, blue, 0);
+ }
+
+ public static int getArgbColorFromCeaColor(int red, int green, int blue, int opacity) {
+ Assertions.checkIndex(red, 0, 4);
+ Assertions.checkIndex(green, 0, 4);
+ Assertions.checkIndex(blue, 0, 4);
+ Assertions.checkIndex(opacity, 0, 4);
+
+ int alpha;
+ switch (opacity) {
+ case 0:
+ case 1:
+ // Note the value of '1' is actually FLASH, but we don't support that.
+ alpha = 255;
+ break;
+ case 2:
+ alpha = 127;
+ break;
+ case 3:
+ alpha = 0;
+ break;
+ default:
+ alpha = 255;
+ }
+
+ // TODO: Add support for the Alternative Minimum Color List or the full 64 RGB combinations.
+
+ // Return values based on the Minimum Color List
+ return Color.argb(alpha,
+ (red > 1 ? 255 : 0),
+ (green > 1 ? 255 : 0),
+ (blue > 1 ? 255 : 0));
+ }
+
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java
new file mode 100644
index 0000000000..5d63ca8e82
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/Cea708InitializationData.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2018 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 java.util.Collections;
+import java.util.List;
+
+/** Initialization data for CEA-708 decoders. */
+public final class Cea708InitializationData {
+
+ /**
+ * Whether the closed caption service is formatted for displays with 16:9 aspect ratio. If false,
+ * the closed caption service is formatted for 4:3 displays.
+ */
+ public final boolean isWideAspectRatio;
+
+ private Cea708InitializationData(List<byte[]> initializationData) {
+ isWideAspectRatio = initializationData.get(0)[0] != 0;
+ }
+
+ /**
+ * Returns an object representation of CEA-708 initialization data
+ *
+ * @param initializationData Binary CEA-708 initialization data.
+ * @return The object representation.
+ */
+ public static Cea708InitializationData fromData(List<byte[]> initializationData) {
+ return new Cea708InitializationData(initializationData);
+ }
+
+ /**
+ * Builds binary CEA-708 initialization data.
+ *
+ * @param isWideAspectRatio Whether the closed caption service is formatted for displays with 16:9
+ * aspect ratio.
+ * @return Binary CEA-708 initializaton data.
+ */
+ public static List<byte[]> buildData(boolean isWideAspectRatio) {
+ return Collections.singletonList(new byte[] {(byte) (isWideAspectRatio ? 1 : 0)});
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaDecoder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaDecoder.java
new file mode 100644
index 0000000000..42fa915fc5
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaDecoder.java
@@ -0,0 +1,204 @@
+/*
+ * 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 androidx.annotation.NonNull;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+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.SubtitleDecoderException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.text.SubtitleOutputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import java.util.ArrayDeque;
+import java.util.PriorityQueue;
+
+/**
+ * Base class for subtitle parsers for CEA captions.
+ */
+/* package */ abstract class CeaDecoder implements SubtitleDecoder {
+
+ private static final int NUM_INPUT_BUFFERS = 10;
+ private static final int NUM_OUTPUT_BUFFERS = 2;
+
+ private final ArrayDeque<CeaInputBuffer> availableInputBuffers;
+ private final ArrayDeque<SubtitleOutputBuffer> availableOutputBuffers;
+ private final PriorityQueue<CeaInputBuffer> queuedInputBuffers;
+
+ private CeaInputBuffer dequeuedInputBuffer;
+ private long playbackPositionUs;
+ private long queuedInputBufferCount;
+
+ public CeaDecoder() {
+ availableInputBuffers = new ArrayDeque<>();
+ for (int i = 0; i < NUM_INPUT_BUFFERS; i++) {
+ availableInputBuffers.add(new CeaInputBuffer());
+ }
+ availableOutputBuffers = new ArrayDeque<>();
+ for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) {
+ availableOutputBuffers.add(new CeaOutputBuffer());
+ }
+ queuedInputBuffers = new PriorityQueue<>();
+ }
+
+ @Override
+ public abstract String getName();
+
+ @Override
+ public void setPositionUs(long positionUs) {
+ playbackPositionUs = positionUs;
+ }
+
+ @Override
+ public SubtitleInputBuffer dequeueInputBuffer() throws SubtitleDecoderException {
+ Assertions.checkState(dequeuedInputBuffer == null);
+ if (availableInputBuffers.isEmpty()) {
+ return null;
+ }
+ dequeuedInputBuffer = availableInputBuffers.pollFirst();
+ return dequeuedInputBuffer;
+ }
+
+ @Override
+ public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException {
+ Assertions.checkArgument(inputBuffer == dequeuedInputBuffer);
+ if (inputBuffer.isDecodeOnly()) {
+ // We can drop this buffer early (i.e. before it would be decoded) as the CEA formats allow
+ // for decoding to begin mid-stream.
+ releaseInputBuffer(dequeuedInputBuffer);
+ } else {
+ dequeuedInputBuffer.queuedInputBufferCount = queuedInputBufferCount++;
+ queuedInputBuffers.add(dequeuedInputBuffer);
+ }
+ dequeuedInputBuffer = null;
+ }
+
+ @Override
+ public SubtitleOutputBuffer dequeueOutputBuffer() throws SubtitleDecoderException {
+ if (availableOutputBuffers.isEmpty()) {
+ return null;
+ }
+ // iterate through all available input buffers whose timestamps are less than or equal
+ // to the current playback position; processing input buffers for future content should
+ // be deferred until they would be applicable
+ while (!queuedInputBuffers.isEmpty()
+ && queuedInputBuffers.peek().timeUs <= playbackPositionUs) {
+ CeaInputBuffer inputBuffer = queuedInputBuffers.poll();
+
+ // If the input buffer indicates we've reached the end of the stream, we can
+ // return immediately with an output buffer propagating that
+ if (inputBuffer.isEndOfStream()) {
+ SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst();
+ outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
+ releaseInputBuffer(inputBuffer);
+ return outputBuffer;
+ }
+
+ decode(inputBuffer);
+
+ // check if we have any caption updates to report
+ if (isNewSubtitleDataAvailable()) {
+ // Even if the subtitle is decode-only; we need to generate it to consume the data so it
+ // isn't accidentally prepended to the next subtitle
+ Subtitle subtitle = createSubtitle();
+ if (!inputBuffer.isDecodeOnly()) {
+ SubtitleOutputBuffer outputBuffer = availableOutputBuffers.pollFirst();
+ outputBuffer.setContent(inputBuffer.timeUs, subtitle, Format.OFFSET_SAMPLE_RELATIVE);
+ releaseInputBuffer(inputBuffer);
+ return outputBuffer;
+ }
+ }
+
+ releaseInputBuffer(inputBuffer);
+ }
+
+ return null;
+ }
+
+ private void releaseInputBuffer(CeaInputBuffer inputBuffer) {
+ inputBuffer.clear();
+ availableInputBuffers.add(inputBuffer);
+ }
+
+ protected void releaseOutputBuffer(SubtitleOutputBuffer outputBuffer) {
+ outputBuffer.clear();
+ availableOutputBuffers.add(outputBuffer);
+ }
+
+ @Override
+ public void flush() {
+ queuedInputBufferCount = 0;
+ playbackPositionUs = 0;
+ while (!queuedInputBuffers.isEmpty()) {
+ releaseInputBuffer(queuedInputBuffers.poll());
+ }
+ if (dequeuedInputBuffer != null) {
+ releaseInputBuffer(dequeuedInputBuffer);
+ dequeuedInputBuffer = null;
+ }
+ }
+
+ @Override
+ public void release() {
+ // Do nothing
+ }
+
+ /**
+ * Returns whether there is data available to create a new {@link Subtitle}.
+ */
+ protected abstract boolean isNewSubtitleDataAvailable();
+
+ /**
+ * Creates a {@link Subtitle} from the available data.
+ */
+ protected abstract Subtitle createSubtitle();
+
+ /**
+ * Filters and processes the raw data, providing {@link Subtitle}s via {@link #createSubtitle()}
+ * when sufficient data has been processed.
+ */
+ protected abstract void decode(SubtitleInputBuffer inputBuffer);
+
+ private static final class CeaInputBuffer extends SubtitleInputBuffer
+ implements Comparable<CeaInputBuffer> {
+
+ private long queuedInputBufferCount;
+
+ @Override
+ public int compareTo(@NonNull CeaInputBuffer other) {
+ if (isEndOfStream() != other.isEndOfStream()) {
+ return isEndOfStream() ? 1 : -1;
+ }
+ long delta = timeUs - other.timeUs;
+ if (delta == 0) {
+ delta = queuedInputBufferCount - other.queuedInputBufferCount;
+ if (delta == 0) {
+ return 0;
+ }
+ }
+ return delta > 0 ? 1 : -1;
+ }
+ }
+
+ private final class CeaOutputBuffer extends SubtitleOutputBuffer {
+
+ @Override
+ public final void release() {
+ releaseOutputBuffer(this);
+ }
+ }
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaSubtitle.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaSubtitle.java
new file mode 100644
index 0000000000..f4649c4c4b
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaSubtitle.java
@@ -0,0 +1,60 @@
+/*
+ * 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 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.util.Assertions;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A representation of a CEA subtitle.
+ */
+/* package */ final class CeaSubtitle implements Subtitle {
+
+ private final List<Cue> cues;
+
+ /**
+ * @param cues The subtitle cues.
+ */
+ public CeaSubtitle(List<Cue> cues) {
+ this.cues = cues;
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ return timeUs < 0 ? 0 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return 1;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ Assertions.checkArgument(index == 0);
+ return 0;
+ }
+
+ @Override
+ public List<Cue> getCues(long timeUs) {
+ return timeUs >= 0 ? cues : Collections.emptyList();
+ }
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaUtil.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaUtil.java
new file mode 100644
index 0000000000..ced169ba17
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/CeaUtil.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2017 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 org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
+
+/** Utility methods for handling CEA-608/708 messages. Defined in A/53 Part 4:2009. */
+public final class CeaUtil {
+
+ private static final String TAG = "CeaUtil";
+
+ public static final int USER_DATA_IDENTIFIER_GA94 = 0x47413934;
+ public static final int USER_DATA_TYPE_CODE_MPEG_CC = 0x3;
+
+ private static final int PAYLOAD_TYPE_CC = 4;
+ private static final int COUNTRY_CODE = 0xB5;
+ private static final int PROVIDER_CODE_ATSC = 0x31;
+ private static final int PROVIDER_CODE_DIRECTV = 0x2F;
+
+ /**
+ * Consumes the unescaped content of an SEI NAL unit, writing the content of any CEA-608 messages
+ * as samples to all of the provided outputs.
+ *
+ * @param presentationTimeUs The presentation time in microseconds for any samples.
+ * @param seiBuffer The unescaped SEI NAL unit data, excluding the NAL unit start code and type.
+ * @param outputs The outputs to which any samples should be written.
+ */
+ public static void consume(long presentationTimeUs, ParsableByteArray seiBuffer,
+ TrackOutput[] outputs) {
+ while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) {
+ int payloadType = readNon255TerminatedValue(seiBuffer);
+ int payloadSize = readNon255TerminatedValue(seiBuffer);
+ int nextPayloadPosition = seiBuffer.getPosition() + payloadSize;
+ // Process the payload.
+ if (payloadSize == -1 || payloadSize > seiBuffer.bytesLeft()) {
+ // This might occur if we're trying to read an encrypted SEI NAL unit.
+ Log.w(TAG, "Skipping remainder of malformed SEI NAL unit.");
+ nextPayloadPosition = seiBuffer.limit();
+ } else if (payloadType == PAYLOAD_TYPE_CC && payloadSize >= 8) {
+ int countryCode = seiBuffer.readUnsignedByte();
+ int providerCode = seiBuffer.readUnsignedShort();
+ int userIdentifier = 0;
+ if (providerCode == PROVIDER_CODE_ATSC) {
+ userIdentifier = seiBuffer.readInt();
+ }
+ int userDataTypeCode = seiBuffer.readUnsignedByte();
+ if (providerCode == PROVIDER_CODE_DIRECTV) {
+ seiBuffer.skipBytes(1); // user_data_length.
+ }
+ boolean messageIsSupportedCeaCaption =
+ countryCode == COUNTRY_CODE
+ && (providerCode == PROVIDER_CODE_ATSC || providerCode == PROVIDER_CODE_DIRECTV)
+ && userDataTypeCode == USER_DATA_TYPE_CODE_MPEG_CC;
+ if (providerCode == PROVIDER_CODE_ATSC) {
+ messageIsSupportedCeaCaption &= userIdentifier == USER_DATA_IDENTIFIER_GA94;
+ }
+ if (messageIsSupportedCeaCaption) {
+ consumeCcData(presentationTimeUs, seiBuffer, outputs);
+ }
+ }
+ seiBuffer.setPosition(nextPayloadPosition);
+ }
+ }
+
+ /**
+ * Consumes caption data (cc_data), writing the content as samples to all of the provided outputs.
+ *
+ * @param presentationTimeUs The presentation time in microseconds for any samples.
+ * @param ccDataBuffer The buffer containing the caption data.
+ * @param outputs The outputs to which any samples should be written.
+ */
+ public static void consumeCcData(
+ long presentationTimeUs, ParsableByteArray ccDataBuffer, TrackOutput[] outputs) {
+ // First byte contains: reserved (1), process_cc_data_flag (1), zero_bit (1), cc_count (5).
+ int firstByte = ccDataBuffer.readUnsignedByte();
+ boolean processCcDataFlag = (firstByte & 0x40) != 0;
+ if (!processCcDataFlag) {
+ // No need to process.
+ return;
+ }
+ int ccCount = firstByte & 0x1F;
+ ccDataBuffer.skipBytes(1); // Ignore em_data
+ // Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2)
+ // + cc_data_1 (8) + cc_data_2 (8).
+ int sampleLength = ccCount * 3;
+ int sampleStartPosition = ccDataBuffer.getPosition();
+ for (TrackOutput output : outputs) {
+ ccDataBuffer.setPosition(sampleStartPosition);
+ output.sampleData(ccDataBuffer, sampleLength);
+ output.sampleMetadata(
+ presentationTimeUs,
+ C.BUFFER_FLAG_KEY_FRAME,
+ sampleLength,
+ /* offset= */ 0,
+ /* encryptionData= */ null);
+ }
+ }
+
+ /**
+ * Reads a value from the provided buffer consisting of zero or more 0xFF bytes followed by a
+ * terminating byte not equal to 0xFF. The returned value is ((0xFF * N) + T), where N is the
+ * number of 0xFF bytes and T is the value of the terminating byte.
+ *
+ * @param buffer The buffer from which to read the value.
+ * @return The read value, or -1 if the end of the buffer is reached before a value is read.
+ */
+ private static int readNon255TerminatedValue(ParsableByteArray buffer) {
+ int b;
+ int value = 0;
+ do {
+ if (buffer.bytesLeft() == 0) {
+ return -1;
+ }
+ b = buffer.readUnsignedByte();
+ value += b;
+ } while (b == 0xFF);
+ return value;
+ }
+
+ private CeaUtil() {}
+
+}
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/package-info.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/package-info.java
new file mode 100644
index 0000000000..e80d06586a
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/text/cea/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+@NonNullApi
+package org.mozilla.thirdparty.com.google.android.exoplayer2.text.cea;
+
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.NonNullApi;