diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/cea/Cea708Decoder.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/cea/Cea708Decoder.java index 4a33430ba3..6d2102be73 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/cea/Cea708Decoder.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/cea/Cea708Decoder.java @@ -15,30 +15,152 @@ */ package androidx.media3.extractor.text.cea; -import static androidx.media3.common.util.Assertions.checkNotNull; - +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 androidx.media3.common.C; import androidx.media3.common.Format; +import androidx.media3.common.text.Cue; +import androidx.media3.common.text.Cue.AnchorType; +import androidx.media3.common.util.Assertions; import androidx.media3.common.util.CodecSpecificDataUtil; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.ParsableBitArray; +import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.UnstableApi; -import androidx.media3.extractor.text.CuesWithTiming; -import androidx.media3.extractor.text.CuesWithTimingSubtitle; import androidx.media3.extractor.text.Subtitle; import androidx.media3.extractor.text.SubtitleDecoder; import androidx.media3.extractor.text.SubtitleInputBuffer; -import androidx.media3.extractor.text.SubtitleParser.OutputOptions; -import com.google.common.collect.ImmutableList; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** A {@link SubtitleDecoder} for CEA-708 (also known as "EIA-708"). */ @UnstableApi public final class Cea708Decoder extends CeaDecoder { - private final Cea708Parser cea708Parser; - @Nullable private CuesWithTiming cues; - private boolean isNewSubtitleDataAvailable; + 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 captionChannelPacketData; + private int previousSequenceNumber; + + // TODO: Use isWideAspectRatio in decoding. + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final boolean isWideAspectRatio; + + private final int selectedServiceNumber; + private final CueInfoBuilder[] cueInfoBuilders; + + private CueInfoBuilder currentCueInfoBuilder; + @Nullable private List cues; + @Nullable private List lastCues; + + @Nullable private DtvCcPacket currentDtvCcPacket; + private int currentWindow; /** * Constructs an instance. @@ -49,7 +171,20 @@ public final class Cea708Decoder extends CeaDecoder { * CodecSpecificDataUtil#buildCea708InitializationData}. */ public Cea708Decoder(int accessibilityChannel, @Nullable List initializationData) { - this.cea708Parser = new Cea708Parser(accessibilityChannel, initializationData); + ccData = new ParsableByteArray(); + captionChannelPacketData = new ParsableBitArray(); + previousSequenceNumber = C.INDEX_UNSET; + selectedServiceNumber = accessibilityChannel == Format.NO_VALUE ? 1 : accessibilityChannel; + isWideAspectRatio = + initializationData != null + && CodecSpecificDataUtil.parseCea708InitializationData(initializationData); + + cueInfoBuilders = new CueInfoBuilder[NUM_WINDOWS]; + for (int i = 0; i < NUM_WINDOWS; i++) { + cueInfoBuilders[i] = new CueInfoBuilder(); + } + + currentCueInfoBuilder = cueInfoBuilders[0]; } @Override @@ -60,36 +195,1277 @@ public final class Cea708Decoder extends CeaDecoder { @Override public void flush() { super.flush(); - isNewSubtitleDataAvailable = false; cues = null; - cea708Parser.reset(); + lastCues = null; + currentWindow = 0; + currentCueInfoBuilder = cueInfoBuilders[currentWindow]; + resetCueBuilders(); + currentDtvCcPacket = null; } @Override protected boolean isNewSubtitleDataAvailable() { - return isNewSubtitleDataAvailable; + return cues != lastCues; } @Override protected Subtitle createSubtitle() { - isNewSubtitleDataAvailable = false; - return new CuesWithTimingSubtitle(ImmutableList.of(checkNotNull(cues))); + lastCues = cues; + return new CeaSubtitle(Assertions.checkNotNull(cues)); } @Override protected void decode(SubtitleInputBuffer inputBuffer) { - ByteBuffer subtitleData = checkNotNull(inputBuffer.data); + // Subtitle input buffers are non-direct and the position is zero, so calling array() is safe. + ByteBuffer subtitleData = Assertions.checkNotNull(inputBuffer.data); + @SuppressWarnings("ByteBufferBackingArray") + byte[] inputBufferData = subtitleData.array(); + ccData.reset(inputBufferData, subtitleData.limit()); + while (ccData.bytesLeft() >= 3) { + int ccTypeAndValid = (ccData.readUnsignedByte() & 0x07); - cea708Parser.parse( - subtitleData.array(), - /* offset= */ subtitleData.arrayOffset(), - /* length= */ subtitleData.limit(), - OutputOptions.allCues(), - /* output= */ cues -> { - isNewSubtitleDataAvailable = true; - this.cues = - new CuesWithTiming( - cues.cues, /* startTimeUs= */ C.TIME_UNSET, /* durationUs= */ C.TIME_UNSET); - }); + 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 + if (previousSequenceNumber != C.INDEX_UNSET + && sequenceNumber != (previousSequenceNumber + 1) % 4) { + resetCueBuilders(); + Log.w( + TAG, + "Sequence number discontinuity. previous=" + + previousSequenceNumber + + " current=" + + sequenceNumber); + } + previousSequenceNumber = sequenceNumber; + + 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; + } + + @RequiresNonNull("currentDtvCcPacket") + private void processCurrentPacket() { + if (currentDtvCcPacket.currentIndex != (currentDtvCcPacket.packetSize * 2 - 1)) { + Log.d( + TAG, + "DtvCcPacket ended prematurely; size is " + + (currentDtvCcPacket.packetSize * 2 - 1) + + ", but current index is " + + currentDtvCcPacket.currentIndex + + " (sequence number " + + currentDtvCcPacket.sequenceNumber + + ");"); + // We've received cc_type=0x03 (packet start) before receiving packetSize byte pairs of data. + // This might indicate a byte pair has been lost, but we'll still attempt to process the data + // we have received. + } + + // 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; + + // Streams with multiple embedded CC tracks (different language tracks) can be delivered + // in the same frame packet, so captionChannelPacketData can contain service blocks with + // different service numbers. + // + // We iterate over the full buffer until we find a null service block or until the buffer is + // exhausted. On each iteration we process a single service block. If the block has a service + // number different to the currently selected service, then we skip it and continue with the + // next service block. + captionChannelPacketData.reset(currentDtvCcPacket.packetData, currentDtvCcPacket.currentIndex); + while (captionChannelPacketData.bitsLeft() > 0) { + // Parse the Standard Service Block Header (see CEA-708B 6.2.1) + int serviceNumber = captionChannelPacketData.readBits(3); + int blockSize = captionChannelPacketData.readBits(5); + if (serviceNumber == 7) { + // Parse the Extended Service Block Header (see CEA-708B 6.2.2) + captionChannelPacketData.skipBits(2); + serviceNumber = captionChannelPacketData.readBits(6); + if (serviceNumber < 7) { + Log.w(TAG, "Invalid extended service number: " + serviceNumber); + } + } + + // Ignore packets with the Null Service Block Header (see CEA-708B 6.2.3) + if (blockSize == 0) { + if (serviceNumber != 0) { + Log.w(TAG, "serviceNumber is non-zero (" + serviceNumber + ") when blockSize is 0"); + } + break; + } + + if (serviceNumber != selectedServiceNumber) { + captionChannelPacketData.skipBytes(blockSize); + continue; + } + + // Process only the information for the current service block (there could be + // more data in the buffer, but it is not part of the current service block). + int endBlockPosition = captionChannelPacketData.getPosition() + (blockSize * 8); + while (captionChannelPacketData.getPosition() < endBlockPosition) { + int command = captionChannelPacketData.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 = captionChannelPacketData.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: + currentCueInfoBuilder.backspace(); + break; + case COMMAND_FF: + resetCueBuilders(); + break; + case COMMAND_CR: + currentCueInfoBuilder.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); + captionChannelPacketData.skipBits(8); + } else if (command >= COMMAND_P16_START && command <= COMMAND_P16_END) { + Log.w(TAG, "Currently unsupported COMMAND_P16 Command: " + command); + captionChannelPacketData.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; + currentCueInfoBuilder = cueInfoBuilders[window]; + } + break; + case COMMAND_CLW: + for (int i = 1; i <= NUM_WINDOWS; i++) { + if (captionChannelPacketData.readBit()) { + cueInfoBuilders[NUM_WINDOWS - i].clear(); + } + } + break; + case COMMAND_DSW: + for (int i = 1; i <= NUM_WINDOWS; i++) { + if (captionChannelPacketData.readBit()) { + cueInfoBuilders[NUM_WINDOWS - i].setVisibility(true); + } + } + break; + case COMMAND_HDW: + for (int i = 1; i <= NUM_WINDOWS; i++) { + if (captionChannelPacketData.readBit()) { + cueInfoBuilders[NUM_WINDOWS - i].setVisibility(false); + } + } + break; + case COMMAND_TGW: + for (int i = 1; i <= NUM_WINDOWS; i++) { + if (captionChannelPacketData.readBit()) { + CueInfoBuilder cueInfoBuilder = cueInfoBuilders[NUM_WINDOWS - i]; + cueInfoBuilder.setVisibility(!cueInfoBuilder.isVisible()); + } + } + break; + case COMMAND_DLW: + for (int i = 1; i <= NUM_WINDOWS; i++) { + if (captionChannelPacketData.readBit()) { + cueInfoBuilders[NUM_WINDOWS - i].reset(); + } + } + break; + case COMMAND_DLY: + // TODO: Add support for delay commands. + captionChannelPacketData.skipBits(8); + break; + case COMMAND_DLC: + // TODO: Add support for delay commands. + break; + case COMMAND_RST: + resetCueBuilders(); + break; + case COMMAND_SPA: + if (!currentCueInfoBuilder.isDefined()) { + // ignore this command if the current window/cue isn't defined + captionChannelPacketData.skipBits(16); + } else { + handleSetPenAttributes(); + } + break; + case COMMAND_SPC: + if (!currentCueInfoBuilder.isDefined()) { + // ignore this command if the current window/cue isn't defined + captionChannelPacketData.skipBits(24); + } else { + handleSetPenColor(); + } + break; + case COMMAND_SPL: + if (!currentCueInfoBuilder.isDefined()) { + // ignore this command if the current window/cue isn't defined + captionChannelPacketData.skipBits(16); + } else { + handleSetPenLocation(); + } + break; + case COMMAND_SWA: + if (!currentCueInfoBuilder.isDefined()) { + // ignore this command if the current window/cue isn't defined + captionChannelPacketData.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; + currentCueInfoBuilder = cueInfoBuilders[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) { + captionChannelPacketData.skipBits(8); + } else if (command <= 0x17) { + captionChannelPacketData.skipBits(16); + } else if (command <= 0x1F) { + captionChannelPacketData.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) { + captionChannelPacketData.skipBits(32); + } else if (command <= 0x8F) { + captionChannelPacketData.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 + captionChannelPacketData.skipBits(2); + int length = captionChannelPacketData.readBits(6); + captionChannelPacketData.skipBits(8 * length); + } + } + + private void handleG0Character(int characterCode) { + if (characterCode == CHARACTER_MN) { + currentCueInfoBuilder.append('\u266B'); + } else { + currentCueInfoBuilder.append((char) (characterCode & 0xFF)); + } + } + + private void handleG1Character(int characterCode) { + currentCueInfoBuilder.append((char) (characterCode & 0xFF)); + } + + private void handleG2Character(int characterCode) { + switch (characterCode) { + case CHARACTER_TSP: + currentCueInfoBuilder.append('\u0020'); + break; + case CHARACTER_NBTSP: + currentCueInfoBuilder.append('\u00A0'); + break; + case CHARACTER_ELLIPSIS: + currentCueInfoBuilder.append('\u2026'); + break; + case CHARACTER_BIG_CARONS: + currentCueInfoBuilder.append('\u0160'); + break; + case CHARACTER_BIG_OE: + currentCueInfoBuilder.append('\u0152'); + break; + case CHARACTER_SOLID_BLOCK: + currentCueInfoBuilder.append('\u2588'); + break; + case CHARACTER_OPEN_SINGLE_QUOTE: + currentCueInfoBuilder.append('\u2018'); + break; + case CHARACTER_CLOSE_SINGLE_QUOTE: + currentCueInfoBuilder.append('\u2019'); + break; + case CHARACTER_OPEN_DOUBLE_QUOTE: + currentCueInfoBuilder.append('\u201C'); + break; + case CHARACTER_CLOSE_DOUBLE_QUOTE: + currentCueInfoBuilder.append('\u201D'); + break; + case CHARACTER_BOLD_BULLET: + currentCueInfoBuilder.append('\u2022'); + break; + case CHARACTER_TM: + currentCueInfoBuilder.append('\u2122'); + break; + case CHARACTER_SMALL_CARONS: + currentCueInfoBuilder.append('\u0161'); + break; + case CHARACTER_SMALL_OE: + currentCueInfoBuilder.append('\u0153'); + break; + case CHARACTER_SM: + currentCueInfoBuilder.append('\u2120'); + break; + case CHARACTER_DIAERESIS_Y: + currentCueInfoBuilder.append('\u0178'); + break; + case CHARACTER_ONE_EIGHTH: + currentCueInfoBuilder.append('\u215B'); + break; + case CHARACTER_THREE_EIGHTHS: + currentCueInfoBuilder.append('\u215C'); + break; + case CHARACTER_FIVE_EIGHTHS: + currentCueInfoBuilder.append('\u215D'); + break; + case CHARACTER_SEVEN_EIGHTHS: + currentCueInfoBuilder.append('\u215E'); + break; + case CHARACTER_VERTICAL_BORDER: + currentCueInfoBuilder.append('\u2502'); + break; + case CHARACTER_UPPER_RIGHT_BORDER: + currentCueInfoBuilder.append('\u2510'); + break; + case CHARACTER_LOWER_LEFT_BORDER: + currentCueInfoBuilder.append('\u2514'); + break; + case CHARACTER_HORIZONTAL_BORDER: + currentCueInfoBuilder.append('\u2500'); + break; + case CHARACTER_LOWER_RIGHT_BORDER: + currentCueInfoBuilder.append('\u2518'); + break; + case CHARACTER_UPPER_LEFT_BORDER: + currentCueInfoBuilder.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) { + currentCueInfoBuilder.append('\u33C4'); + } else { + Log.w(TAG, "Invalid G3 character: " + characterCode); + // Substitute any unsupported G3 character with an underscore as per CEA-708 specification. + currentCueInfoBuilder.append('_'); + } + } + + private void handleSetPenAttributes() { + // the SetPenAttributes command contains 2 bytes of data + // first byte + int textTag = captionChannelPacketData.readBits(4); + int offset = captionChannelPacketData.readBits(2); + int penSize = captionChannelPacketData.readBits(2); + // second byte + boolean italicsToggle = captionChannelPacketData.readBit(); + boolean underlineToggle = captionChannelPacketData.readBit(); + int edgeType = captionChannelPacketData.readBits(3); + int fontStyle = captionChannelPacketData.readBits(3); + + currentCueInfoBuilder.setPenAttributes( + textTag, offset, penSize, italicsToggle, underlineToggle, edgeType, fontStyle); + } + + private void handleSetPenColor() { + // the SetPenColor command contains 3 bytes of data + // first byte + int foregroundO = captionChannelPacketData.readBits(2); + int foregroundR = captionChannelPacketData.readBits(2); + int foregroundG = captionChannelPacketData.readBits(2); + int foregroundB = captionChannelPacketData.readBits(2); + int foregroundColor = + CueInfoBuilder.getArgbColorFromCeaColor(foregroundR, foregroundG, foregroundB, foregroundO); + // second byte + int backgroundO = captionChannelPacketData.readBits(2); + int backgroundR = captionChannelPacketData.readBits(2); + int backgroundG = captionChannelPacketData.readBits(2); + int backgroundB = captionChannelPacketData.readBits(2); + int backgroundColor = + CueInfoBuilder.getArgbColorFromCeaColor(backgroundR, backgroundG, backgroundB, backgroundO); + // third byte + captionChannelPacketData.skipBits(2); // null padding + int edgeR = captionChannelPacketData.readBits(2); + int edgeG = captionChannelPacketData.readBits(2); + int edgeB = captionChannelPacketData.readBits(2); + int edgeColor = CueInfoBuilder.getArgbColorFromCeaColor(edgeR, edgeG, edgeB); + + currentCueInfoBuilder.setPenColor(foregroundColor, backgroundColor, edgeColor); + } + + private void handleSetPenLocation() { + // the SetPenLocation command contains 2 bytes of data + // first byte + captionChannelPacketData.skipBits(4); + int row = captionChannelPacketData.readBits(4); + // second byte + captionChannelPacketData.skipBits(2); + int column = captionChannelPacketData.readBits(6); + + currentCueInfoBuilder.setPenLocation(row, column); + } + + private void handleSetWindowAttributes() { + // the SetWindowAttributes command contains 4 bytes of data + // first byte + int fillO = captionChannelPacketData.readBits(2); + int fillR = captionChannelPacketData.readBits(2); + int fillG = captionChannelPacketData.readBits(2); + int fillB = captionChannelPacketData.readBits(2); + int fillColor = CueInfoBuilder.getArgbColorFromCeaColor(fillR, fillG, fillB, fillO); + // second byte + int borderType = captionChannelPacketData.readBits(2); // only the lower 2 bits of borderType + int borderR = captionChannelPacketData.readBits(2); + int borderG = captionChannelPacketData.readBits(2); + int borderB = captionChannelPacketData.readBits(2); + int borderColor = CueInfoBuilder.getArgbColorFromCeaColor(borderR, borderG, borderB); + // third byte + if (captionChannelPacketData.readBit()) { + borderType |= 0x04; // set the top bit of the 3-bit borderType + } + boolean wordWrapToggle = captionChannelPacketData.readBit(); + int printDirection = captionChannelPacketData.readBits(2); + int scrollDirection = captionChannelPacketData.readBits(2); + int justification = captionChannelPacketData.readBits(2); + // fourth byte + // Note that we don't intend to support display effects + captionChannelPacketData.skipBits(8); // effectSpeed(4), effectDirection(2), displayEffect(2) + + currentCueInfoBuilder.setWindowAttributes( + fillColor, + borderColor, + wordWrapToggle, + borderType, + printDirection, + scrollDirection, + justification); + } + + private void handleDefineWindow(int window) { + CueInfoBuilder cueInfoBuilder = cueInfoBuilders[window]; + + // the DefineWindow command contains 6 bytes of data + // first byte + captionChannelPacketData.skipBits(2); // null padding + boolean visible = captionChannelPacketData.readBit(); + boolean rowLock = captionChannelPacketData.readBit(); + boolean columnLock = captionChannelPacketData.readBit(); + int priority = captionChannelPacketData.readBits(3); + // second byte + boolean relativePositioning = captionChannelPacketData.readBit(); + int verticalAnchor = captionChannelPacketData.readBits(7); + // third byte + int horizontalAnchor = captionChannelPacketData.readBits(8); + // fourth byte + int anchorId = captionChannelPacketData.readBits(4); + int rowCount = captionChannelPacketData.readBits(4); + // fifth byte + captionChannelPacketData.skipBits(2); // null padding + int columnCount = captionChannelPacketData.readBits(6); + // sixth byte + captionChannelPacketData.skipBits(2); // null padding + int windowStyle = captionChannelPacketData.readBits(3); + int penStyle = captionChannelPacketData.readBits(3); + + cueInfoBuilder.defineWindow( + visible, + rowLock, + columnLock, + priority, + relativePositioning, + verticalAnchor, + horizontalAnchor, + rowCount, + columnCount, + anchorId, + windowStyle, + penStyle); + } + + private List getDisplayCues() { + List displayCueInfos = new ArrayList<>(); + for (int i = 0; i < NUM_WINDOWS; i++) { + if (!cueInfoBuilders[i].isEmpty() && cueInfoBuilders[i].isVisible()) { + @Nullable Cea708CueInfo cueInfo = cueInfoBuilders[i].build(); + if (cueInfo != null) { + displayCueInfos.add(cueInfo); + } + } + } + Collections.sort(displayCueInfos, Cea708CueInfo.LEAST_IMPORTANT_FIRST); + List displayCues = new ArrayList<>(displayCueInfos.size()); + for (int i = 0; i < displayCueInfos.size(); i++) { + displayCues.add(displayCueInfos.get(i).cue); + } + return Collections.unmodifiableList(displayCues); + } + + private void resetCueBuilders() { + for (int i = 0; i < NUM_WINDOWS; i++) { + cueInfoBuilders[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.CueInfoBuilder and + // Cea608Decoder.CueBuilder which could be refactored into a separate class. + private static final class CueInfoBuilder { + + private static final int RELATIVE_CUE_SIZE = 99; + private static final int VERTICAL_SIZE = 74; + 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; + private int row; + + public CueInfoBuilder() { + 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.INDEX_UNSET; + underlineStartPosition = C.INDEX_UNSET; + foregroundColorStartPosition = C.INDEX_UNSET; + backgroundColorStartPosition = C.INDEX_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.INDEX_UNSET) { + if (!italicsToggle) { + captionStringBuilder.setSpan( + new StyleSpan(Typeface.ITALIC), + italicsStartPosition, + captionStringBuilder.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + italicsStartPosition = C.INDEX_UNSET; + } + } else if (italicsToggle) { + italicsStartPosition = captionStringBuilder.length(); + } + + if (underlineStartPosition != C.INDEX_UNSET) { + if (!underlineToggle) { + captionStringBuilder.setSpan( + new UnderlineSpan(), + underlineStartPosition, + captionStringBuilder.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + underlineStartPosition = C.INDEX_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.INDEX_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.INDEX_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.INDEX_UNSET) { + italicsStartPosition = 0; + } + if (underlineStartPosition != C.INDEX_UNSET) { + underlineStartPosition = 0; + } + if (foregroundColorStartPosition != C.INDEX_UNSET) { + foregroundColorStartPosition = 0; + } + if (backgroundColorStartPosition != C.INDEX_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.INDEX_UNSET) { + spannableStringBuilder.setSpan( + new StyleSpan(Typeface.ITALIC), + italicsStartPosition, + length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + if (underlineStartPosition != C.INDEX_UNSET) { + spannableStringBuilder.setSpan( + new UnderlineSpan(), + underlineStartPosition, + length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + if (foregroundColorStartPosition != C.INDEX_UNSET) { + spannableStringBuilder.setSpan( + new ForegroundColorSpan(foregroundColor), + foregroundColorStartPosition, + length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + if (backgroundColorStartPosition != C.INDEX_UNSET) { + spannableStringBuilder.setSpan( + new BackgroundColorSpan(backgroundColor), + backgroundColorStartPosition, + length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + return new SpannableString(spannableStringBuilder); + } + + @Nullable + public Cea708CueInfo 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 Cea708CueInfo( + 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)); + } + } + + /** A {@link Cue} for CEA-708. */ + private static final class Cea708CueInfo { + + /** + * Sorts cue infos in order of ascending {@link Cea708CueInfo#priority} (which is descending by + * numeric value). + */ + private static final Comparator LEAST_IMPORTANT_FIRST = + (thisInfo, thatInfo) -> Integer.compare(thatInfo.priority, thisInfo.priority); + + public final Cue cue; + + /** + * The priority of the cue box. Low values are higher priority. + * + *

If cue boxes overlap, higher priority cue boxes are drawn on top. + * + *

See 8.4.2 of the CEA-708B spec. + */ + public final int priority; + + /** + * @param text See {@link Cue#text}. + * @param textAlignment See {@link Cue#textAlignment}. + * @param line See {@link Cue#line}. + * @param lineType See {@link Cue#lineType}. + * @param lineAnchor See {@link Cue#lineAnchor}. + * @param position See {@link Cue#position}. + * @param positionAnchor See {@link Cue#positionAnchor}. + * @param size See {@link Cue#size}. + * @param windowColorSet See {@link Cue#windowColorSet}. + * @param windowColor See {@link Cue#windowColor}. + * @param priority See {@link #priority}. + */ + public Cea708CueInfo( + CharSequence text, + Alignment textAlignment, + float line, + @Cue.LineType int lineType, + @AnchorType int lineAnchor, + float position, + @AnchorType int positionAnchor, + float size, + boolean windowColorSet, + int windowColor, + int priority) { + Cue.Builder cueBuilder = + new Cue.Builder() + .setText(text) + .setTextAlignment(textAlignment) + .setLine(line, lineType) + .setLineAnchor(lineAnchor) + .setPosition(position) + .setPositionAnchor(positionAnchor) + .setSize(size); + if (windowColorSet) { + cueBuilder.setWindowColor(windowColor); + } + this.cue = cueBuilder.build(); + this.priority = priority; + } } } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/cea/Cea708Parser.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/cea/Cea708Parser.java deleted file mode 100644 index 1a0a23ca84..0000000000 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/cea/Cea708Parser.java +++ /dev/null @@ -1,1472 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package androidx.media3.extractor.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 androidx.media3.common.C; -import androidx.media3.common.Format; -import androidx.media3.common.Format.CueReplacementBehavior; -import androidx.media3.common.text.Cue; -import androidx.media3.common.text.Cue.AnchorType; -import androidx.media3.common.util.Assertions; -import androidx.media3.common.util.CodecSpecificDataUtil; -import androidx.media3.common.util.Consumer; -import androidx.media3.common.util.Log; -import androidx.media3.common.util.ParsableBitArray; -import androidx.media3.common.util.ParsableByteArray; -import androidx.media3.common.util.UnstableApi; -import androidx.media3.extractor.text.CuesWithTiming; -import androidx.media3.extractor.text.Subtitle; -import androidx.media3.extractor.text.SubtitleParser; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import org.checkerframework.checker.nullness.qual.RequiresNonNull; - -/** A {@link SubtitleParser} for CEA-708 (also known as "EIA-708"). */ -// TODO: b/317488646 - Either re-combine this with Cea708Decoder (if we decide that decoding this -// format must happen during rendering), or re-add it to DefaultSubtitleParserFactory (if we're -// able to solve the re-ordering issue during extraction). -@UnstableApi -/* package */ final class Cea708Parser implements SubtitleParser { - - /** - * The {@link CueReplacementBehavior} for consecutive {@link CuesWithTiming} emitted by this - * implementation. - */ - public static final @CueReplacementBehavior int CUE_REPLACEMENT_BEHAVIOR = - Format.CUE_REPLACEMENT_BEHAVIOR_REPLACE; - - private static final String TAG = "Cea708Parser"; - - 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 captionChannelPacketData; - private int previousSequenceNumber; - - // TODO: Use isWideAspectRatio in decoding. - @SuppressWarnings({"unused", "FieldCanBeLocal"}) - private final boolean isWideAspectRatio; - - private final int selectedServiceNumber; - private final CueInfoBuilder[] cueInfoBuilders; - - private CueInfoBuilder currentCueInfoBuilder; - @Nullable private List cues; - @Nullable private List lastCues; - - @Nullable private DtvCcPacket currentDtvCcPacket; - private int currentWindow; - - public Cea708Parser(int accessibilityChannel, @Nullable List initializationData) { - ccData = new ParsableByteArray(); - captionChannelPacketData = new ParsableBitArray(); - previousSequenceNumber = C.INDEX_UNSET; - selectedServiceNumber = accessibilityChannel == Format.NO_VALUE ? 1 : accessibilityChannel; - isWideAspectRatio = - initializationData != null - && CodecSpecificDataUtil.parseCea708InitializationData(initializationData); - - cueInfoBuilders = new CueInfoBuilder[NUM_WINDOWS]; - for (int i = 0; i < NUM_WINDOWS; i++) { - cueInfoBuilders[i] = new CueInfoBuilder(); - } - - currentCueInfoBuilder = cueInfoBuilders[0]; - } - - @Override - public void reset() { - cues = null; - lastCues = null; - currentWindow = 0; - currentCueInfoBuilder = cueInfoBuilders[currentWindow]; - resetCueBuilders(); - currentDtvCcPacket = null; - } - - @Override - public @CueReplacementBehavior int getCueReplacementBehavior() { - return CUE_REPLACEMENT_BEHAVIOR; - } - - @Override - public Subtitle parseToLegacySubtitle(byte[] data, int offset, int length) { - throw new UnsupportedOperationException( - "Cannot produce Subtitle instances directly. Use Cea708Decoder instead."); - } - - @Override - public void parse( - byte[] data, - int offset, - int length, - OutputOptions outputOptions, - Consumer output) { - ccData.reset(data, offset + length); - ccData.setPosition(offset); - 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 - if (previousSequenceNumber != C.INDEX_UNSET - && sequenceNumber != (previousSequenceNumber + 1) % 4) { - resetCueBuilders(); - Log.w( - TAG, - "Sequence number discontinuity. previous=" - + previousSequenceNumber - + " current=" - + sequenceNumber); - } - previousSequenceNumber = sequenceNumber; - - 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(); - } - if (cues != lastCues) { - lastCues = cues; - // TODO! - output.accept( - new CuesWithTiming( - Assertions.checkNotNull(cues), - /* startTimeUs= */ C.TIME_UNSET, - /* durationUs= */ C.TIME_UNSET)); - } - } - } - - private void finalizeCurrentPacket() { - if (currentDtvCcPacket == null) { - // No packet to finalize; - return; - } - - processCurrentPacket(); - currentDtvCcPacket = null; - } - - @RequiresNonNull("currentDtvCcPacket") - private void processCurrentPacket() { - if (currentDtvCcPacket.currentIndex != (currentDtvCcPacket.packetSize * 2 - 1)) { - Log.d( - TAG, - "DtvCcPacket ended prematurely; size is " - + (currentDtvCcPacket.packetSize * 2 - 1) - + ", but current index is " - + currentDtvCcPacket.currentIndex - + " (sequence number " - + currentDtvCcPacket.sequenceNumber - + ");"); - // We've received cc_type=0x03 (packet start) before receiving packetSize byte pairs of data. - // This might indicate a byte pair has been lost, but we'll still attempt to process the data - // we have received. - } - - // 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; - - // Streams with multiple embedded CC tracks (different language tracks) can be delivered - // in the same frame packet, so captionChannelPacketData can contain service blocks with - // different service numbers. - // - // We iterate over the full buffer until we find a null service block or until the buffer is - // exhausted. On each iteration we process a single service block. If the block has a service - // number different to the currently selected service, then we skip it and continue with the - // next service block. - captionChannelPacketData.reset(currentDtvCcPacket.packetData, currentDtvCcPacket.currentIndex); - while (captionChannelPacketData.bitsLeft() > 0) { - // Parse the Standard Service Block Header (see CEA-708B 6.2.1) - int serviceNumber = captionChannelPacketData.readBits(3); - int blockSize = captionChannelPacketData.readBits(5); - if (serviceNumber == 7) { - // Parse the Extended Service Block Header (see CEA-708B 6.2.2) - captionChannelPacketData.skipBits(2); - serviceNumber = captionChannelPacketData.readBits(6); - if (serviceNumber < 7) { - Log.w(TAG, "Invalid extended service number: " + serviceNumber); - } - } - - // Ignore packets with the Null Service Block Header (see CEA-708B 6.2.3) - if (blockSize == 0) { - if (serviceNumber != 0) { - Log.w(TAG, "serviceNumber is non-zero (" + serviceNumber + ") when blockSize is 0"); - } - break; - } - - if (serviceNumber != selectedServiceNumber) { - captionChannelPacketData.skipBytes(blockSize); - continue; - } - - // Process only the information for the current service block (there could be - // more data in the buffer, but it is not part of the current service block). - int endBlockPosition = captionChannelPacketData.getPosition() + (blockSize * 8); - while (captionChannelPacketData.getPosition() < endBlockPosition) { - int command = captionChannelPacketData.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 = captionChannelPacketData.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: - currentCueInfoBuilder.backspace(); - break; - case COMMAND_FF: - resetCueBuilders(); - break; - case COMMAND_CR: - currentCueInfoBuilder.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); - captionChannelPacketData.skipBits(8); - } else if (command >= COMMAND_P16_START && command <= COMMAND_P16_END) { - Log.w(TAG, "Currently unsupported COMMAND_P16 Command: " + command); - captionChannelPacketData.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; - currentCueInfoBuilder = cueInfoBuilders[window]; - } - break; - case COMMAND_CLW: - for (int i = 1; i <= NUM_WINDOWS; i++) { - if (captionChannelPacketData.readBit()) { - cueInfoBuilders[NUM_WINDOWS - i].clear(); - } - } - break; - case COMMAND_DSW: - for (int i = 1; i <= NUM_WINDOWS; i++) { - if (captionChannelPacketData.readBit()) { - cueInfoBuilders[NUM_WINDOWS - i].setVisibility(true); - } - } - break; - case COMMAND_HDW: - for (int i = 1; i <= NUM_WINDOWS; i++) { - if (captionChannelPacketData.readBit()) { - cueInfoBuilders[NUM_WINDOWS - i].setVisibility(false); - } - } - break; - case COMMAND_TGW: - for (int i = 1; i <= NUM_WINDOWS; i++) { - if (captionChannelPacketData.readBit()) { - CueInfoBuilder cueInfoBuilder = cueInfoBuilders[NUM_WINDOWS - i]; - cueInfoBuilder.setVisibility(!cueInfoBuilder.isVisible()); - } - } - break; - case COMMAND_DLW: - for (int i = 1; i <= NUM_WINDOWS; i++) { - if (captionChannelPacketData.readBit()) { - cueInfoBuilders[NUM_WINDOWS - i].reset(); - } - } - break; - case COMMAND_DLY: - // TODO: Add support for delay commands. - captionChannelPacketData.skipBits(8); - break; - case COMMAND_DLC: - // TODO: Add support for delay commands. - break; - case COMMAND_RST: - resetCueBuilders(); - break; - case COMMAND_SPA: - if (!currentCueInfoBuilder.isDefined()) { - // ignore this command if the current window/cue isn't defined - captionChannelPacketData.skipBits(16); - } else { - handleSetPenAttributes(); - } - break; - case COMMAND_SPC: - if (!currentCueInfoBuilder.isDefined()) { - // ignore this command if the current window/cue isn't defined - captionChannelPacketData.skipBits(24); - } else { - handleSetPenColor(); - } - break; - case COMMAND_SPL: - if (!currentCueInfoBuilder.isDefined()) { - // ignore this command if the current window/cue isn't defined - captionChannelPacketData.skipBits(16); - } else { - handleSetPenLocation(); - } - break; - case COMMAND_SWA: - if (!currentCueInfoBuilder.isDefined()) { - // ignore this command if the current window/cue isn't defined - captionChannelPacketData.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; - currentCueInfoBuilder = cueInfoBuilders[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) { - captionChannelPacketData.skipBits(8); - } else if (command <= 0x17) { - captionChannelPacketData.skipBits(16); - } else if (command <= 0x1F) { - captionChannelPacketData.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) { - captionChannelPacketData.skipBits(32); - } else if (command <= 0x8F) { - captionChannelPacketData.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 - captionChannelPacketData.skipBits(2); - int length = captionChannelPacketData.readBits(6); - captionChannelPacketData.skipBits(8 * length); - } - } - - private void handleG0Character(int characterCode) { - if (characterCode == CHARACTER_MN) { - currentCueInfoBuilder.append('\u266B'); - } else { - currentCueInfoBuilder.append((char) (characterCode & 0xFF)); - } - } - - private void handleG1Character(int characterCode) { - currentCueInfoBuilder.append((char) (characterCode & 0xFF)); - } - - private void handleG2Character(int characterCode) { - switch (characterCode) { - case CHARACTER_TSP: - currentCueInfoBuilder.append('\u0020'); - break; - case CHARACTER_NBTSP: - currentCueInfoBuilder.append('\u00A0'); - break; - case CHARACTER_ELLIPSIS: - currentCueInfoBuilder.append('\u2026'); - break; - case CHARACTER_BIG_CARONS: - currentCueInfoBuilder.append('\u0160'); - break; - case CHARACTER_BIG_OE: - currentCueInfoBuilder.append('\u0152'); - break; - case CHARACTER_SOLID_BLOCK: - currentCueInfoBuilder.append('\u2588'); - break; - case CHARACTER_OPEN_SINGLE_QUOTE: - currentCueInfoBuilder.append('\u2018'); - break; - case CHARACTER_CLOSE_SINGLE_QUOTE: - currentCueInfoBuilder.append('\u2019'); - break; - case CHARACTER_OPEN_DOUBLE_QUOTE: - currentCueInfoBuilder.append('\u201C'); - break; - case CHARACTER_CLOSE_DOUBLE_QUOTE: - currentCueInfoBuilder.append('\u201D'); - break; - case CHARACTER_BOLD_BULLET: - currentCueInfoBuilder.append('\u2022'); - break; - case CHARACTER_TM: - currentCueInfoBuilder.append('\u2122'); - break; - case CHARACTER_SMALL_CARONS: - currentCueInfoBuilder.append('\u0161'); - break; - case CHARACTER_SMALL_OE: - currentCueInfoBuilder.append('\u0153'); - break; - case CHARACTER_SM: - currentCueInfoBuilder.append('\u2120'); - break; - case CHARACTER_DIAERESIS_Y: - currentCueInfoBuilder.append('\u0178'); - break; - case CHARACTER_ONE_EIGHTH: - currentCueInfoBuilder.append('\u215B'); - break; - case CHARACTER_THREE_EIGHTHS: - currentCueInfoBuilder.append('\u215C'); - break; - case CHARACTER_FIVE_EIGHTHS: - currentCueInfoBuilder.append('\u215D'); - break; - case CHARACTER_SEVEN_EIGHTHS: - currentCueInfoBuilder.append('\u215E'); - break; - case CHARACTER_VERTICAL_BORDER: - currentCueInfoBuilder.append('\u2502'); - break; - case CHARACTER_UPPER_RIGHT_BORDER: - currentCueInfoBuilder.append('\u2510'); - break; - case CHARACTER_LOWER_LEFT_BORDER: - currentCueInfoBuilder.append('\u2514'); - break; - case CHARACTER_HORIZONTAL_BORDER: - currentCueInfoBuilder.append('\u2500'); - break; - case CHARACTER_LOWER_RIGHT_BORDER: - currentCueInfoBuilder.append('\u2518'); - break; - case CHARACTER_UPPER_LEFT_BORDER: - currentCueInfoBuilder.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) { - currentCueInfoBuilder.append('\u33C4'); - } else { - Log.w(TAG, "Invalid G3 character: " + characterCode); - // Substitute any unsupported G3 character with an underscore as per CEA-708 specification. - currentCueInfoBuilder.append('_'); - } - } - - private void handleSetPenAttributes() { - // the SetPenAttributes command contains 2 bytes of data - // first byte - int textTag = captionChannelPacketData.readBits(4); - int offset = captionChannelPacketData.readBits(2); - int penSize = captionChannelPacketData.readBits(2); - // second byte - boolean italicsToggle = captionChannelPacketData.readBit(); - boolean underlineToggle = captionChannelPacketData.readBit(); - int edgeType = captionChannelPacketData.readBits(3); - int fontStyle = captionChannelPacketData.readBits(3); - - currentCueInfoBuilder.setPenAttributes( - textTag, offset, penSize, italicsToggle, underlineToggle, edgeType, fontStyle); - } - - private void handleSetPenColor() { - // the SetPenColor command contains 3 bytes of data - // first byte - int foregroundO = captionChannelPacketData.readBits(2); - int foregroundR = captionChannelPacketData.readBits(2); - int foregroundG = captionChannelPacketData.readBits(2); - int foregroundB = captionChannelPacketData.readBits(2); - int foregroundColor = - CueInfoBuilder.getArgbColorFromCeaColor(foregroundR, foregroundG, foregroundB, foregroundO); - // second byte - int backgroundO = captionChannelPacketData.readBits(2); - int backgroundR = captionChannelPacketData.readBits(2); - int backgroundG = captionChannelPacketData.readBits(2); - int backgroundB = captionChannelPacketData.readBits(2); - int backgroundColor = - CueInfoBuilder.getArgbColorFromCeaColor(backgroundR, backgroundG, backgroundB, backgroundO); - // third byte - captionChannelPacketData.skipBits(2); // null padding - int edgeR = captionChannelPacketData.readBits(2); - int edgeG = captionChannelPacketData.readBits(2); - int edgeB = captionChannelPacketData.readBits(2); - int edgeColor = CueInfoBuilder.getArgbColorFromCeaColor(edgeR, edgeG, edgeB); - - currentCueInfoBuilder.setPenColor(foregroundColor, backgroundColor, edgeColor); - } - - private void handleSetPenLocation() { - // the SetPenLocation command contains 2 bytes of data - // first byte - captionChannelPacketData.skipBits(4); - int row = captionChannelPacketData.readBits(4); - // second byte - captionChannelPacketData.skipBits(2); - int column = captionChannelPacketData.readBits(6); - - currentCueInfoBuilder.setPenLocation(row, column); - } - - private void handleSetWindowAttributes() { - // the SetWindowAttributes command contains 4 bytes of data - // first byte - int fillO = captionChannelPacketData.readBits(2); - int fillR = captionChannelPacketData.readBits(2); - int fillG = captionChannelPacketData.readBits(2); - int fillB = captionChannelPacketData.readBits(2); - int fillColor = CueInfoBuilder.getArgbColorFromCeaColor(fillR, fillG, fillB, fillO); - // second byte - int borderType = captionChannelPacketData.readBits(2); // only the lower 2 bits of borderType - int borderR = captionChannelPacketData.readBits(2); - int borderG = captionChannelPacketData.readBits(2); - int borderB = captionChannelPacketData.readBits(2); - int borderColor = CueInfoBuilder.getArgbColorFromCeaColor(borderR, borderG, borderB); - // third byte - if (captionChannelPacketData.readBit()) { - borderType |= 0x04; // set the top bit of the 3-bit borderType - } - boolean wordWrapToggle = captionChannelPacketData.readBit(); - int printDirection = captionChannelPacketData.readBits(2); - int scrollDirection = captionChannelPacketData.readBits(2); - int justification = captionChannelPacketData.readBits(2); - // fourth byte - // Note that we don't intend to support display effects - captionChannelPacketData.skipBits(8); // effectSpeed(4), effectDirection(2), displayEffect(2) - - currentCueInfoBuilder.setWindowAttributes( - fillColor, - borderColor, - wordWrapToggle, - borderType, - printDirection, - scrollDirection, - justification); - } - - private void handleDefineWindow(int window) { - CueInfoBuilder cueInfoBuilder = cueInfoBuilders[window]; - - // the DefineWindow command contains 6 bytes of data - // first byte - captionChannelPacketData.skipBits(2); // null padding - boolean visible = captionChannelPacketData.readBit(); - - // ANSI/CTA-708-E S-2023 spec (Section 8.4.7) indicates that rowLock and columnLock values in - // the media should be ignored and assumed to be true. - captionChannelPacketData.skipBits(2); - - int priority = captionChannelPacketData.readBits(3); - // second byte - boolean relativePositioning = captionChannelPacketData.readBit(); - int verticalAnchor = captionChannelPacketData.readBits(7); - // third byte - int horizontalAnchor = captionChannelPacketData.readBits(8); - // fourth byte - int anchorId = captionChannelPacketData.readBits(4); - int rowCount = captionChannelPacketData.readBits(4); - // fifth byte - captionChannelPacketData.skipBits(2); // null padding - // TODO: Add support for column count. - captionChannelPacketData.skipBits(6); // column count - // sixth byte - captionChannelPacketData.skipBits(2); // null padding - int windowStyle = captionChannelPacketData.readBits(3); - int penStyle = captionChannelPacketData.readBits(3); - - cueInfoBuilder.defineWindow( - visible, - priority, - relativePositioning, - verticalAnchor, - horizontalAnchor, - rowCount, - anchorId, - windowStyle, - penStyle); - } - - private List getDisplayCues() { - List displayCueInfos = new ArrayList<>(); - for (int i = 0; i < NUM_WINDOWS; i++) { - if (!cueInfoBuilders[i].isEmpty() && cueInfoBuilders[i].isVisible()) { - @Nullable Cea708CueInfo cueInfo = cueInfoBuilders[i].build(); - if (cueInfo != null) { - displayCueInfos.add(cueInfo); - } - } - } - Collections.sort(displayCueInfos, Cea708CueInfo.LEAST_IMPORTANT_FIRST); - List displayCues = new ArrayList<>(displayCueInfos.size()); - for (int i = 0; i < displayCueInfos.size(); i++) { - displayCues.add(displayCueInfos.get(i).cue); - } - return Collections.unmodifiableList(displayCues); - } - - private void resetCueBuilders() { - for (int i = 0; i < NUM_WINDOWS; i++) { - cueInfoBuilders[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.CueInfoBuilder and - // Cea608Parser.CueBuilder which could be refactored into a separate class. - private static final class CueInfoBuilder { - - private static final int RELATIVE_CUE_SIZE = 99; - private static final int VERTICAL_SIZE = 74; - 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 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 CueInfoBuilder() { - 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; - 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.INDEX_UNSET; - underlineStartPosition = C.INDEX_UNSET; - foregroundColorStartPosition = C.INDEX_UNSET; - backgroundColorStartPosition = C.INDEX_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, - int priority, - boolean relativePositioning, - int verticalAnchor, - int horizontalAnchor, - int rowCount, - int anchorId, - int windowStyleId, - int penStyleId) { - this.defined = true; - this.visible = visible; - 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 ((rolledUpCaptions.size() >= this.rowCount) - || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) { - rolledUpCaptions.remove(0); - } - } - - 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.INDEX_UNSET) { - if (!italicsToggle) { - captionStringBuilder.setSpan( - new StyleSpan(Typeface.ITALIC), - italicsStartPosition, - captionStringBuilder.length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - italicsStartPosition = C.INDEX_UNSET; - } - } else if (italicsToggle) { - italicsStartPosition = captionStringBuilder.length(); - } - - if (underlineStartPosition != C.INDEX_UNSET) { - if (!underlineToggle) { - captionStringBuilder.setSpan( - new UnderlineSpan(), - underlineStartPosition, - captionStringBuilder.length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - underlineStartPosition = C.INDEX_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.INDEX_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.INDEX_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.INDEX_UNSET) { - italicsStartPosition = 0; - } - if (underlineStartPosition != C.INDEX_UNSET) { - underlineStartPosition = 0; - } - if (foregroundColorStartPosition != C.INDEX_UNSET) { - foregroundColorStartPosition = 0; - } - if (backgroundColorStartPosition != C.INDEX_UNSET) { - backgroundColorStartPosition = 0; - } - - while ((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.INDEX_UNSET) { - spannableStringBuilder.setSpan( - new StyleSpan(Typeface.ITALIC), - italicsStartPosition, - length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - if (underlineStartPosition != C.INDEX_UNSET) { - spannableStringBuilder.setSpan( - new UnderlineSpan(), - underlineStartPosition, - length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - if (foregroundColorStartPosition != C.INDEX_UNSET) { - spannableStringBuilder.setSpan( - new ForegroundColorSpan(foregroundColor), - foregroundColorStartPosition, - length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - if (backgroundColorStartPosition != C.INDEX_UNSET) { - spannableStringBuilder.setSpan( - new BackgroundColorSpan(backgroundColor), - backgroundColorStartPosition, - length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - - return new SpannableString(spannableStringBuilder); - } - - @Nullable - public Cea708CueInfo 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 Cea708CueInfo( - 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)); - } - } - - /** A {@link Cue} for CEA-708. */ - private static final class Cea708CueInfo { - - /** - * Sorts cue infos in order of ascending {@link Cea708CueInfo#priority} (which is descending by - * numeric value). - */ - private static final Comparator LEAST_IMPORTANT_FIRST = - (thisInfo, thatInfo) -> Integer.compare(thatInfo.priority, thisInfo.priority); - - public final Cue cue; - - /** - * The priority of the cue box. Low values are higher priority. - * - *

If cue boxes overlap, higher priority cue boxes are drawn on top. - * - *

See 8.4.2 of the CEA-708B spec. - */ - public final int priority; - - /** - * @param text See {@link Cue#text}. - * @param textAlignment See {@link Cue#textAlignment}. - * @param line See {@link Cue#line}. - * @param lineType See {@link Cue#lineType}. - * @param lineAnchor See {@link Cue#lineAnchor}. - * @param position See {@link Cue#position}. - * @param positionAnchor See {@link Cue#positionAnchor}. - * @param size See {@link Cue#size}. - * @param windowColorSet See {@link Cue#windowColorSet}. - * @param windowColor See {@link Cue#windowColor}. - * @param priority See {@link #priority}. - */ - public Cea708CueInfo( - CharSequence text, - Alignment textAlignment, - float line, - @Cue.LineType int lineType, - @AnchorType int lineAnchor, - float position, - @AnchorType int positionAnchor, - float size, - boolean windowColorSet, - int windowColor, - int priority) { - Cue.Builder cueBuilder = - new Cue.Builder() - .setText(text) - .setTextAlignment(textAlignment) - .setLine(line, lineType) - .setLineAnchor(lineAnchor) - .setPosition(position) - .setPositionAnchor(positionAnchor) - .setSize(size); - if (windowColorSet) { - cueBuilder.setWindowColor(windowColor); - } - this.cue = cueBuilder.build(); - this.priority = priority; - } - } -} diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/text/cea/Cea708ParserTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/text/cea/Cea708ParserTest.java deleted file mode 100644 index 802d41cba0..0000000000 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/text/cea/Cea708ParserTest.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright 2023 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 - * - * https://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 androidx.media3.extractor.text.cea; - -import static androidx.media3.common.util.Assertions.checkState; -import static androidx.media3.test.utils.TestUtil.createByteArray; -import static com.google.common.truth.Truth.assertThat; - -import androidx.media3.common.Format; -import androidx.media3.common.util.Util; -import androidx.media3.extractor.text.CuesWithTiming; -import androidx.media3.extractor.text.SubtitleParser; -import androidx.media3.test.utils.TestUtil; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.google.common.base.Charsets; -import com.google.common.collect.Iterables; -import com.google.common.primitives.Bytes; -import com.google.common.primitives.UnsignedBytes; -import java.util.ArrayList; -import java.util.List; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** Tests for {@link Cea708Parser}. */ -@RunWith(AndroidJUnit4.class) -public class Cea708ParserTest { - - private static final byte CHANNEL_PACKET_START = 0x7; - private static final byte CHANNEL_PACKET_DATA = 0x6; - private static final byte CHANNEL_PACKET_END = 0x2; - - @Test - public void singleServiceAndWindowDefinition() throws Exception { - Cea708Parser cea708Parser = - new Cea708Parser( - /* accessibilityChannel= */ Format.NO_VALUE, /* initializationData= */ null); - byte[] windowDefinition = - TestUtil.createByteArray( - 0x98, // DF0 command (define window 0) - 0b0010_0000, // visible=true, row lock and column lock disabled, priority=0 - 0xF0 | 50, // relative positioning, anchor vertical - 50, // anchor horizontal - 10, // anchor point = 0, row count = 10 - 30, // column count = 30 - 0b0000_1001); // window style = 1, pen style = 1 - byte[] setCurrentWindow = TestUtil.createByteArray(0x80); // CW0 (set current window to 0) - byte[] subtitleData = - encodePacketIntoBytePairs( - createPacket( - /* sequenceNumber= */ 0, - createServiceBlock( - Bytes.concat( - windowDefinition, - setCurrentWindow, - "test subtitle".getBytes(Charsets.UTF_8))))); - - List result = new ArrayList<>(); - cea708Parser.parse(subtitleData, SubtitleParser.OutputOptions.allCues(), result::add); - - assertThat(Iterables.getOnlyElement(Iterables.getOnlyElement(result).cues).text.toString()) - .isEqualTo("test subtitle"); - } - - @Test - public void singleServiceAndWindowDefinition_respectsOffsetAndLimit() throws Exception { - Cea708Parser cea708Parser = - new Cea708Parser( - /* accessibilityChannel= */ Format.NO_VALUE, /* initializationData= */ null); - byte[] windowDefinition = - TestUtil.createByteArray( - 0x98, // DF0 command (define window 0) - 0b0010_0000, // visible=true, row lock and column lock disabled, priority=0 - 0xF0 | 50, // relative positioning, anchor vertical - 50, // anchor horizontal - 10, // anchor point = 0, row count = 10 - 30, // column count = 30 - 0b0000_1001); // window style = 1, pen style = 1 - byte[] setCurrentWindow = TestUtil.createByteArray(0x80); // CW0 (set current window to 0) - byte[] subtitleData = - encodePacketIntoBytePairs( - createPacket( - /* sequenceNumber= */ 0, - createServiceBlock( - Bytes.concat( - windowDefinition, - setCurrentWindow, - "test subtitle".getBytes(Charsets.UTF_8))))); - byte[] garbagePrefix = TestUtil.buildTestData(subtitleData.length * 2); - byte[] garbageSuffix = TestUtil.buildTestData(10); - byte[] subtitleDataWithGarbagePrefixAndSuffix = - Bytes.concat(garbagePrefix, subtitleData, garbageSuffix); - - List result = new ArrayList<>(); - cea708Parser.parse( - subtitleDataWithGarbagePrefixAndSuffix, - garbagePrefix.length, - subtitleData.length, - SubtitleParser.OutputOptions.allCues(), - result::add); - - assertThat(Iterables.getOnlyElement(Iterables.getOnlyElement(result).cues).text.toString()) - .isEqualTo("test subtitle"); - } - - @Test - public void singleServiceAndWindowDefinition_ignoreRowLock() throws Exception { - Cea708Parser cea708Parser = - new Cea708Parser( - /* accessibilityChannel= */ Format.NO_VALUE, /* initializationData= */ null); - byte[] windowDefinition = - TestUtil.createByteArray( - 0x98, // DF0 command (define window 0) - 0b0010_0000, // visible=true, row lock and column lock disabled, priority=0 - 0xF0 | 50, // relative positioning, anchor vertical - 50, // anchor horizontal - 1, // anchor point = 0, row count = 1 - 30, // column count = 30 - 0b0000_1001); // window style = 1, pen style = 1 - byte[] setCurrentWindow = TestUtil.createByteArray(0x80); // CW0 (set current window to 0) - byte[] subtitleData = - encodePacketIntoBytePairs( - createPacket( - /* sequenceNumber= */ 0, - createServiceBlock( - Bytes.concat( - windowDefinition, - setCurrentWindow, - "row1\r\nrow2\r\nrow3\r\nrow4".getBytes(Charsets.UTF_8))))); - - List result = new ArrayList<>(); - cea708Parser.parse(subtitleData, SubtitleParser.OutputOptions.allCues(), result::add); - - // Row count is 1 (which means 2 rows should be kept). Row lock is disabled in the media, - // but this is ignored and the result is still truncated to only the last two rows. - assertThat(Iterables.getOnlyElement(Iterables.getOnlyElement(result).cues).text.toString()) - .isEqualTo("row3\nrow4"); - } - - /** See section 4.4.1 of the CEA-708-B spec. */ - private static byte[] encodePacketIntoBytePairs(byte[] packet) { - checkState(packet.length % 2 == 0); - byte[] bytePairs = new byte[Util.ceilDivide(packet.length * 3, 2) + 3]; - int outputIndex = 0; - for (int packetIndex = 0; packetIndex < packet.length; packetIndex++) { - if (packetIndex == 0) { - bytePairs[outputIndex++] = CHANNEL_PACKET_START; - } else if (packetIndex % 2 == 0) { - bytePairs[outputIndex++] = CHANNEL_PACKET_DATA; - } - bytePairs[outputIndex++] = packet[packetIndex]; - } - bytePairs[bytePairs.length - 3] = CHANNEL_PACKET_END; - bytePairs[bytePairs.length - 2] = 0x0; - bytePairs[bytePairs.length - 1] = 0x0; - return bytePairs; - } - - /** - * Creates a DTVCC Caption Channel Packet with the provided {@code data}. - * - *

See section 5 of the CEA-708-B spec. - */ - private static byte[] createPacket(int sequenceNumber, byte[] data) { - checkState(sequenceNumber >= 0); - checkState(sequenceNumber <= 0b11); - checkState(data.length <= 0b11111); - - int encodedSize = data.length >= 126 ? 0 : Util.ceilDivide(data.length + 1, 2); - int packetHeader = sequenceNumber << 6 | encodedSize; - if (data.length % 2 != 0) { - return Bytes.concat(createByteArray(packetHeader), data); - } else { - return Bytes.concat(createByteArray(packetHeader), data, createByteArray(0)); - } - } - - /** Creates a service block containing {@code data} with {@code serviceNumber = 1}. */ - private static byte[] createServiceBlock(byte[] data) { - return Bytes.concat( - createByteArray(bitPackServiceBlockHeader(/* serviceNumber= */ 1, data.length)), data); - } - - /** - * Returns an unsigned byte with {@code serviceNumber} packed into the upper 3 bits, and {@code - * blockSize} in the lower 5 bits. - * - *

See section 6.2.1 of the CEA-708-B spec. - */ - private static byte bitPackServiceBlockHeader(int serviceNumber, int blockSize) { - checkState(serviceNumber > 0); // service number 0 is reserved - checkState(serviceNumber < 7); // we only test the standard (non-extended) header - checkState(blockSize >= 0); - checkState(blockSize < 1 << 5); - return UnsignedBytes.checkedCast((serviceNumber << 5) | blockSize); - } -}