From 18a24a1fde68753737030bac9c7d56e6e7c292e4 Mon Sep 17 00:00:00 2001 From: cdrolle Date: Tue, 17 Jan 2017 10:35:18 -0800 Subject: [PATCH] Added the CEA-708 support to the open-source project. Issue: #1807 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=144726542 --- .../text/SubtitleDecoderFactory.java | 7 + .../exoplayer2/text/cea/Cea708Cue.java | 67 + .../exoplayer2/text/cea/Cea708Decoder.java | 1225 +++++++++++++++++ 3 files changed, 1299 insertions(+) create mode 100644 library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java diff --git a/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index 2b28eb3c300..077fc8848bc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -17,6 +17,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.cea.Cea608Decoder; +import com.google.android.exoplayer2.text.cea.Cea708Decoder; import com.google.android.exoplayer2.text.subrip.SubripDecoder; import com.google.android.exoplayer2.text.ttml.TtmlDecoder; import com.google.android.exoplayer2.text.tx3g.Tx3gDecoder; @@ -58,6 +59,7 @@ public interface SubtitleDecoderFactory { *
  • SubRip ({@link SubripDecoder})
  • *
  • TX3G ({@link Tx3gDecoder})
  • *
  • Cea608 ({@link Cea608Decoder})
  • + *
  • Cea708 ({@link Cea708Decoder})
  • * */ SubtitleDecoderFactory DEFAULT = new SubtitleDecoderFactory() { @@ -78,6 +80,9 @@ public SubtitleDecoder createDecoder(Format format) { || format.sampleMimeType.equals(MimeTypes.APPLICATION_MP4CEA608)) { return clazz.asSubclass(SubtitleDecoder.class).getConstructor(String.class, Integer.TYPE) .newInstance(format.sampleMimeType, format.accessibilityChannel); + } else if (format.sampleMimeType.equals(MimeTypes.APPLICATION_CEA708)) { + return clazz.asSubclass(SubtitleDecoder.class).getConstructor(Integer.TYPE) + .newInstance(format.accessibilityChannel); } else { return clazz.asSubclass(SubtitleDecoder.class).getConstructor().newInstance(); } @@ -105,6 +110,8 @@ private Class getDecoderClass(String mimeType) { case MimeTypes.APPLICATION_CEA608: case MimeTypes.APPLICATION_MP4CEA608: return Class.forName("com.google.android.exoplayer2.text.cea.Cea608Decoder"); + case MimeTypes.APPLICATION_CEA708: + return Class.forName("com.google.android.exoplayer2.text.cea.Cea708Decoder"); default: return null; } diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java new file mode 100644 index 00000000000..e63d1d4118f --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.text.cea; + +import android.text.Layout.Alignment; +import com.google.android.exoplayer2.text.Cue; + +/** + * A {@link Cue} for CEA-708. + */ +/* package */ final class Cea708Cue extends Cue implements Comparable { + + /** + * An unset priority. + */ + public static final int PRIORITY_UNSET = -1; + + /** + * 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(Cea708Cue other) { + if (other.priority < priority) { + return -1; + } else if (other.priority > priority) { + return 1; + } + return 0; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java new file mode 100644 index 00000000000..5ca5ce12707 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -0,0 +1,1225 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.text.cea; + +import android.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 android.util.Log; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.Cue.AnchorType; +import com.google.android.exoplayer2.text.Subtitle; +import com.google.android.exoplayer2.text.SubtitleDecoder; +import com.google.android.exoplayer2.text.SubtitleInputBuffer; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.ParsableBitArray; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * A {@link SubtitleDecoder} for CEA-708 (also known as "EIA-708"). + * + *

    This implementation does not provide full compatibility with the CEA-708 specification. Note + * that only the default pen/text and window/cue colors (i.e. text with + * {@link CueBuilder#COLOR_SOLID_WHITE} foreground and {@link CueBuilder#COLOR_SOLID_BLACK} + * background, and cues with {@link CueBuilder#COLOR_SOLID_BLACK} fill) will be overridden with + * device accessibility settings; all others will use the colors and opacity specified by the + * caption data. + */ +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_DS4 = 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 cues; + private List lastCues; + + private DtvCcPacket currentDtvCcPacket; + private int currentWindow; + + public Cea708Decoder(int accessibilityChannel) { + 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) { + ccData.reset(inputBuffer.data.array(), 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) { + finalizeCurrentPacket(); + 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 + ")"); + } + + 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); + } + + // 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; + } + + while (serviceBlockPacket.bitsLeft() > 0) { + int command = serviceBlockPacket.readBits(8); + if (command != COMMAND_EXT1) { + if (command <= GROUP_C0_END) { + handleC0Command(command); + } else if (command <= GROUP_G0_END) { + handleG0Character(command); + } else if (command <= GROUP_C1_END) { + handleC1Command(command); + // Cues are always updated after a C1 command + cues = getDisplayCues(); + } else if (command <= GROUP_G1_END) { + handleG1Character(command); + } 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); + } else if (command <= GROUP_C3_END) { + handleC3Command(command); + } else if (command <= GROUP_G3_END) { + handleG3Character(command); + } else { + Log.w(TAG, "Invalid extended command: " + command); + } + } + } + } + + 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_DS4: + case COMMAND_DF5: + case COMMAND_DF6: + case COMMAND_DF7: + window = (command - COMMAND_DF0); + handleDefineWindow(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 <= 0x0F) { + // 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 getDisplayCues() { + List 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 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; + + public CueBuilder() { + rolledUpCaptions = new LinkedList<>(); + 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; + } + + 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. + } + + 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)); + } + + } + +}