Home | History | Annotate | Download | only in cc
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tv.tuner.cc;
     18 
     19 import android.os.SystemClock;
     20 import android.support.annotation.IntDef;
     21 import android.util.Log;
     22 import android.util.SparseIntArray;
     23 import com.android.tv.tuner.data.Cea708Data;
     24 import com.android.tv.tuner.data.Cea708Data.CaptionColor;
     25 import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
     26 import com.android.tv.tuner.data.Cea708Data.CaptionPenAttr;
     27 import com.android.tv.tuner.data.Cea708Data.CaptionPenColor;
     28 import com.android.tv.tuner.data.Cea708Data.CaptionPenLocation;
     29 import com.android.tv.tuner.data.Cea708Data.CaptionWindow;
     30 import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr;
     31 import com.android.tv.tuner.data.Cea708Data.CcPacket;
     32 import com.android.tv.tuner.util.ByteArrayBuffer;
     33 import java.io.UnsupportedEncodingException;
     34 import java.lang.annotation.Retention;
     35 import java.lang.annotation.RetentionPolicy;
     36 import java.nio.ByteBuffer;
     37 import java.nio.charset.StandardCharsets;
     38 import java.util.Arrays;
     39 import java.util.TreeSet;
     40 
     41 /**
     42  * A class for parsing CEA-708, which is the standard for closed captioning for ATSC DTV.
     43  *
     44  * <p>ATSC DTV closed caption data are carried on picture user data of video streams. This class
     45  * starts to parse from picture user data payload, so extraction process of user_data from video
     46  * streams is up to outside of this code.
     47  *
     48  * <p>There are 4 steps to decode user_data to provide closed caption services.
     49  *
     50  * <h3>Step 1. user_data -&gt; CcPacket ({@link #parseClosedCaption} method)</h3>
     51  *
     52  * <p>First, user_data consists of cc_data packets, which are 3-byte segments. Here, CcPacket is a
     53  * collection of cc_data packets in a frame along with same presentation timestamp. Because cc_data
     54  * packets must be reassembled in the frame display order, CcPackets are reordered.
     55  *
     56  * <h3>Step 2. CcPacket -&gt; DTVCC packet ({@link #parseCcPacket} method)</h3>
     57  *
     58  * <p>Each cc_data packet has a one byte for declaring a type of itself and data validity, and the
     59  * subsequent two bytes for input data of a DTVCC packet. There are 4 types for cc_data packet.
     60  * We're interested in DTVCC_PACKET_START(type 3) and DTVCC_PACKET_DATA(type 2). Each DTVCC packet
     61  * begins with DTVCC_PACKET_START(type 3) and the following cc_data packets which has
     62  * DTVCC_PACKET_DATA(type 2) are appended into the DTVCC packet being assembled.
     63  *
     64  * <h3>Step 3. DTVCC packet -&gt; Service Blocks ({@link #parseDtvCcPacket} method)</h3>
     65  *
     66  * <p>A DTVCC packet consists of multiple service blocks. Each service block represents a caption
     67  * track and has a service number, which ranges from 1 to 63, that denotes caption track identity.
     68  * In here, we listen at most one chosen caption track by {@link #mListenServiceNumber}. Otherwise,
     69  * just skip the other service blocks.
     70  *
     71  * <h3>Step 4. Interpreting Service Block Data ({@link #parseServiceBlockData}, {@code parseXX}, and
     72  * {@link #parseExt1} methods)</h3>
     73  *
     74  * <p>Service block data is actual caption stream. it looks similar to telnet. It uses most parts of
     75  * ASCII table and consists of specially defined commands and some ASCII control codes which work in
     76  * a behavior slightly different from their original purpose. ASCII control codes and caption
     77  * commands are explicit instructions that control the state of a closed caption service and the
     78  * other ASCII and text codes are implicit instructions that send their characters to buffer.
     79  *
     80  * <p>There are 4 main code groups and 4 extended code groups. Both the range of code groups are the
     81  * same as the range of a byte.
     82  *
     83  * <p>4 main code groups: C0, C1, G0, G1 <br>
     84  * 4 extended code groups: C2, C3, G2, G3
     85  *
     86  * <p>Each code group has its own handle method. For example, {@link #parseC0} handles C0 code group
     87  * and so on. And {@link #parseServiceBlockData} method maps a stream on the main code groups while
     88  * {@link #parseExt1} method maps on the extended code groups.
     89  *
     90  * <p>The main code groups:
     91  *
     92  * <ul>
     93  *   <li>C0 - contains modified ASCII control codes. It is not intended by CEA-708 but Korea TTA
     94  *       standard for ATSC CC uses P16 character heavily, which is unclear entity in CEA-708 doc,
     95  *       even for the alphanumeric characters instead of ASCII characters.
     96  *   <li>C1 - contains the caption commands. There are 3 categories of a caption command.
     97  *       <ul>
     98  *         <li>Window commands: The window commands control a caption window which is addressable
     99  *             area being with in the Safe title area. (CWX, CLW, DSW, HDW, TGW, DLW, SWA, DFX)
    100  *         <li>Pen commands: Th pen commands control text style and location. (SPA, SPC, SPL)
    101  *         <li>Job commands: The job commands make a delay and recover from the delay. (DLY, DLC,
    102  *             RST)
    103  *       </ul>
    104  *   <li>G0 - same as printable ASCII character set except music note character.
    105  *   <li>G1 - same as ISO 8859-1 Latin 1 character set.
    106  * </ul>
    107  *
    108  * <p>Most of the extended code groups are being skipped.
    109  */
    110 public class Cea708Parser {
    111     private static final String TAG = "Cea708Parser";
    112     private static final boolean DEBUG = false;
    113 
    114     // According to CEA-708B, the maximum value of closed caption bandwidth is 9600bps.
    115     private static final int MAX_ALLOCATED_SIZE = 9600 / 8;
    116     private static final String MUSIC_NOTE_CHAR =
    117             new String("\u266B".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
    118 
    119     // The following values are denoting the type of closed caption data.
    120     // See CEA-708B section 4.4.1.
    121     private static final int CC_TYPE_DTVCC_PACKET_START = 3;
    122     private static final int CC_TYPE_DTVCC_PACKET_DATA = 2;
    123 
    124     // The following values are defined in CEA-708B Figure 4 and 6.
    125     private static final int DTVCC_MAX_PACKET_SIZE = 64;
    126     private static final int DTVCC_PACKET_SIZE_SCALE_FACTOR = 2;
    127     private static final int DTVCC_EXTENDED_SERVICE_NUMBER_POINT = 7;
    128 
    129     // The following values are for seeking closed caption tracks.
    130     private static final int DISCOVERY_PERIOD_MS = 10000; // 10 sec
    131     private static final int DISCOVERY_NUM_BYTES_THRESHOLD = 10; // 10 bytes
    132     private static final int DISCOVERY_CC_SERVICE_NUMBER_START = 1; // CC1
    133     private static final int DISCOVERY_CC_SERVICE_NUMBER_END = 4; // CC4
    134 
    135     private final ByteArrayBuffer mDtvCcPacket = new ByteArrayBuffer(MAX_ALLOCATED_SIZE);
    136     private final TreeSet<CcPacket> mCcPackets = new TreeSet<>();
    137     private final StringBuffer mBuffer = new StringBuffer();
    138     private final SparseIntArray mDiscoveredNumBytes = new SparseIntArray(); // per service number
    139     private long mLastDiscoveryLaunchedMs = SystemClock.elapsedRealtime();
    140     private int mCommand = 0;
    141     private int mListenServiceNumber = 0;
    142     private boolean mDtvCcPacking = false;
    143     private boolean mFirstServiceNumberDiscovered;
    144 
    145     // Assign a dummy listener in order to avoid null checks.
    146     private OnCea708ParserListener mListener =
    147             new OnCea708ParserListener() {
    148                 @Override
    149                 public void emitEvent(CaptionEvent event) {
    150                     // do nothing
    151                 }
    152 
    153                 @Override
    154                 public void discoverServiceNumber(int serviceNumber) {
    155                     // do nothing
    156                 }
    157             };
    158 
    159     /**
    160      * {@link Cea708Parser} emits caption event of three different types. {@link
    161      * OnCea708ParserListener#emitEvent} is invoked with the parameter {@link CaptionEvent} to pass
    162      * all the results to an observer of the decoding process.
    163      *
    164      * <p>{@link CaptionEvent#type} determines the type of the result and {@link CaptionEvent#obj}
    165      * contains the output value of a caption event. The observer must do the casting to the
    166      * corresponding type.
    167      *
    168      * <ul>
    169      *   <li>{@code CAPTION_EMIT_TYPE_BUFFER}: Passes a caption text buffer to a observer. {@code
    170      *       obj} must be of {@link String}.
    171      *   <li>{@code CAPTION_EMIT_TYPE_CONTROL}: Passes a caption character control code to a
    172      *       observer. {@code obj} must be of {@link Character}.
    173      *   <li>{@code CAPTION_EMIT_TYPE_CLEAR_COMMAND}: Passes a clear command to a observer. {@code
    174      *       obj} must be {@code NULL}.
    175      * </ul>
    176      */
    177     @IntDef({
    178         CAPTION_EMIT_TYPE_BUFFER,
    179         CAPTION_EMIT_TYPE_CONTROL,
    180         CAPTION_EMIT_TYPE_COMMAND_CWX,
    181         CAPTION_EMIT_TYPE_COMMAND_CLW,
    182         CAPTION_EMIT_TYPE_COMMAND_DSW,
    183         CAPTION_EMIT_TYPE_COMMAND_HDW,
    184         CAPTION_EMIT_TYPE_COMMAND_TGW,
    185         CAPTION_EMIT_TYPE_COMMAND_DLW,
    186         CAPTION_EMIT_TYPE_COMMAND_DLY,
    187         CAPTION_EMIT_TYPE_COMMAND_DLC,
    188         CAPTION_EMIT_TYPE_COMMAND_RST,
    189         CAPTION_EMIT_TYPE_COMMAND_SPA,
    190         CAPTION_EMIT_TYPE_COMMAND_SPC,
    191         CAPTION_EMIT_TYPE_COMMAND_SPL,
    192         CAPTION_EMIT_TYPE_COMMAND_SWA,
    193         CAPTION_EMIT_TYPE_COMMAND_DFX
    194     })
    195     @Retention(RetentionPolicy.SOURCE)
    196     public @interface CaptionEmitType {}
    197 
    198     public static final int CAPTION_EMIT_TYPE_BUFFER = 1;
    199     public static final int CAPTION_EMIT_TYPE_CONTROL = 2;
    200     public static final int CAPTION_EMIT_TYPE_COMMAND_CWX = 3;
    201     public static final int CAPTION_EMIT_TYPE_COMMAND_CLW = 4;
    202     public static final int CAPTION_EMIT_TYPE_COMMAND_DSW = 5;
    203     public static final int CAPTION_EMIT_TYPE_COMMAND_HDW = 6;
    204     public static final int CAPTION_EMIT_TYPE_COMMAND_TGW = 7;
    205     public static final int CAPTION_EMIT_TYPE_COMMAND_DLW = 8;
    206     public static final int CAPTION_EMIT_TYPE_COMMAND_DLY = 9;
    207     public static final int CAPTION_EMIT_TYPE_COMMAND_DLC = 10;
    208     public static final int CAPTION_EMIT_TYPE_COMMAND_RST = 11;
    209     public static final int CAPTION_EMIT_TYPE_COMMAND_SPA = 12;
    210     public static final int CAPTION_EMIT_TYPE_COMMAND_SPC = 13;
    211     public static final int CAPTION_EMIT_TYPE_COMMAND_SPL = 14;
    212     public static final int CAPTION_EMIT_TYPE_COMMAND_SWA = 15;
    213     public static final int CAPTION_EMIT_TYPE_COMMAND_DFX = 16;
    214 
    215     public interface OnCea708ParserListener {
    216         void emitEvent(CaptionEvent event);
    217 
    218         void discoverServiceNumber(int serviceNumber);
    219     }
    220 
    221     public void setListener(OnCea708ParserListener listener) {
    222         if (listener != null) {
    223             mListener = listener;
    224         }
    225     }
    226 
    227     public void clear() {
    228         mDtvCcPacket.clear();
    229         mCcPackets.clear();
    230         mBuffer.setLength(0);
    231         mDiscoveredNumBytes.clear();
    232         mCommand = 0;
    233         mDtvCcPacking = false;
    234     }
    235 
    236     public void setListenServiceNumber(int serviceNumber) {
    237         mListenServiceNumber = serviceNumber;
    238     }
    239 
    240     private void emitCaptionEvent(CaptionEvent captionEvent) {
    241         // Emit the existing string buffer before a new event is arrived.
    242         emitCaptionBuffer();
    243         mListener.emitEvent(captionEvent);
    244     }
    245 
    246     private void emitCaptionBuffer() {
    247         if (mBuffer.length() > 0) {
    248             mListener.emitEvent(new CaptionEvent(CAPTION_EMIT_TYPE_BUFFER, mBuffer.toString()));
    249             mBuffer.setLength(0);
    250         }
    251     }
    252 
    253     // Step 1. user_data -> CcPacket ({@link #parseClosedCaption} method)
    254     public void parseClosedCaption(ByteBuffer data, long framePtsUs) {
    255         int ccCount = data.limit() / 3;
    256         byte[] ccBytes = new byte[3 * ccCount];
    257         for (int i = 0; i < 3 * ccCount; i++) {
    258             ccBytes[i] = data.get(i);
    259         }
    260         CcPacket ccPacket = new CcPacket(ccBytes, ccCount, framePtsUs);
    261         mCcPackets.add(ccPacket);
    262     }
    263 
    264     public boolean processClosedCaptions(long framePtsUs) {
    265         // To get the sorted cc packets that have lower frame pts than current frame pts,
    266         // the following offset divides off the lower side of the packets.
    267         CcPacket offsetPacket = new CcPacket(new byte[0], 0, framePtsUs);
    268         offsetPacket = mCcPackets.lower(offsetPacket);
    269         boolean processed = false;
    270         if (offsetPacket != null) {
    271             while (!mCcPackets.isEmpty() && offsetPacket.compareTo(mCcPackets.first()) >= 0) {
    272                 CcPacket packet = mCcPackets.pollFirst();
    273                 parseCcPacket(packet);
    274                 processed = true;
    275             }
    276         }
    277         return processed;
    278     }
    279 
    280     // Step 2. CcPacket -> DTVCC packet ({@link #parseCcPacket} method)
    281     private void parseCcPacket(CcPacket ccPacket) {
    282         // For the details of cc packet, see ATSC TSG-676 - Table A8.
    283         byte[] bytes = ccPacket.bytes;
    284         int pos = 0;
    285         for (int i = 0; i < ccPacket.ccCount; ++i) {
    286             boolean ccValid = (bytes[pos] & 0x04) != 0;
    287             int ccType = bytes[pos] & 0x03;
    288 
    289             // The dtvcc should be considered complete:
    290             // - if either ccValid is set and ccType is 3
    291             // - or ccValid is clear and ccType is 2 or 3.
    292             if (ccValid) {
    293                 if (ccType == CC_TYPE_DTVCC_PACKET_START) {
    294                     if (mDtvCcPacking) {
    295                         parseDtvCcPacket(mDtvCcPacket.buffer(), mDtvCcPacket.length());
    296                         mDtvCcPacket.clear();
    297                     }
    298                     mDtvCcPacking = true;
    299                     mDtvCcPacket.append(bytes[pos + 1]);
    300                     mDtvCcPacket.append(bytes[pos + 2]);
    301                 } else if (mDtvCcPacking && ccType == CC_TYPE_DTVCC_PACKET_DATA) {
    302                     mDtvCcPacket.append(bytes[pos + 1]);
    303                     mDtvCcPacket.append(bytes[pos + 2]);
    304                 }
    305             } else {
    306                 if ((ccType == CC_TYPE_DTVCC_PACKET_START || ccType == CC_TYPE_DTVCC_PACKET_DATA)
    307                         && mDtvCcPacking) {
    308                     mDtvCcPacking = false;
    309                     parseDtvCcPacket(mDtvCcPacket.buffer(), mDtvCcPacket.length());
    310                     mDtvCcPacket.clear();
    311                 }
    312             }
    313             pos += 3;
    314         }
    315     }
    316 
    317     // Step 3. DTVCC packet -> Service Blocks ({@link #parseDtvCcPacket} method)
    318     private void parseDtvCcPacket(byte[] data, int limit) {
    319         // For the details of DTVCC packet, see CEA-708B Figure 4.
    320         int pos = 0;
    321         int packetSize = data[pos] & 0x3f;
    322         if (packetSize == 0) {
    323             packetSize = DTVCC_MAX_PACKET_SIZE;
    324         }
    325         int calculatedPacketSize = packetSize * DTVCC_PACKET_SIZE_SCALE_FACTOR;
    326         if (limit != calculatedPacketSize) {
    327             return;
    328         }
    329         ++pos;
    330         int len = pos + calculatedPacketSize;
    331         while (pos < len) {
    332             // For the details of Service Block, see CEA-708B Figure 5 and 6.
    333             int serviceNumber = (data[pos] & 0xe0) >> 5;
    334             int blockSize = data[pos] & 0x1f;
    335             ++pos;
    336             if (serviceNumber == DTVCC_EXTENDED_SERVICE_NUMBER_POINT) {
    337                 serviceNumber = (data[pos] & 0x3f);
    338                 ++pos;
    339 
    340                 // Return if invalid service number
    341                 if (serviceNumber < DTVCC_EXTENDED_SERVICE_NUMBER_POINT) {
    342                     return;
    343                 }
    344             }
    345             if (pos + blockSize > limit) {
    346                 return;
    347             }
    348 
    349             // Send parsed service number in order to find unveiled closed caption tracks which
    350             // are not specified in any ATSC PSIP sections. Since some broadcasts send empty closed
    351             // caption tracks, it detects the proper closed caption tracks by counting the number of
    352             // bytes sent with the same service number during a discovery period.
    353             // The viewer in most TV sets chooses between CC1, CC2, CC3, CC4 to view different
    354             // language captions. Therefore, only CC1, CC2, CC3, CC4 are allowed to be reported.
    355             if (blockSize > 0
    356                     && serviceNumber >= DISCOVERY_CC_SERVICE_NUMBER_START
    357                     && serviceNumber <= DISCOVERY_CC_SERVICE_NUMBER_END) {
    358                 mDiscoveredNumBytes.put(
    359                         serviceNumber, blockSize + mDiscoveredNumBytes.get(serviceNumber, 0));
    360             }
    361             if (mLastDiscoveryLaunchedMs + DISCOVERY_PERIOD_MS < SystemClock.elapsedRealtime()
    362                     || !mFirstServiceNumberDiscovered) {
    363                 for (int i = 0; i < mDiscoveredNumBytes.size(); ++i) {
    364                     int discoveredNumBytes = mDiscoveredNumBytes.valueAt(i);
    365                     if (discoveredNumBytes >= DISCOVERY_NUM_BYTES_THRESHOLD) {
    366                         int discoveredServiceNumber = mDiscoveredNumBytes.keyAt(i);
    367                         mListener.discoverServiceNumber(discoveredServiceNumber);
    368                         mFirstServiceNumberDiscovered = true;
    369                     }
    370                 }
    371                 mDiscoveredNumBytes.clear();
    372                 mLastDiscoveryLaunchedMs = SystemClock.elapsedRealtime();
    373             }
    374 
    375             // Skip current service block if either there is no block data or the service number
    376             // is not same as listening service number.
    377             if (blockSize == 0 || serviceNumber != mListenServiceNumber) {
    378                 pos += blockSize;
    379                 continue;
    380             }
    381 
    382             // From this point, starts to read DTVCC coding layer.
    383             // First, identify code groups, which is defined in CEA-708B Section 7.1.
    384             int blockLimit = pos + blockSize;
    385             while (pos < blockLimit) {
    386                 pos = parseServiceBlockData(data, pos);
    387             }
    388 
    389             // Emit the buffer after reading codes.
    390             emitCaptionBuffer();
    391             pos = blockLimit;
    392         }
    393     }
    394 
    395     // Step 4. Main code groups
    396     private int parseServiceBlockData(byte[] data, int pos) {
    397         // For the details of the ranges of DTVCC code groups, see CEA-708B Table 6.
    398         mCommand = data[pos] & 0xff;
    399         ++pos;
    400         if (mCommand == Cea708Data.CODE_C0_EXT1) {
    401             pos = parseExt1(data, pos);
    402         } else if (mCommand >= Cea708Data.CODE_C0_RANGE_START
    403                 && mCommand <= Cea708Data.CODE_C0_RANGE_END) {
    404             pos = parseC0(data, pos);
    405         } else if (mCommand >= Cea708Data.CODE_C1_RANGE_START
    406                 && mCommand <= Cea708Data.CODE_C1_RANGE_END) {
    407             pos = parseC1(data, pos);
    408         } else if (mCommand >= Cea708Data.CODE_G0_RANGE_START
    409                 && mCommand <= Cea708Data.CODE_G0_RANGE_END) {
    410             pos = parseG0(data, pos);
    411         } else if (mCommand >= Cea708Data.CODE_G1_RANGE_START
    412                 && mCommand <= Cea708Data.CODE_G1_RANGE_END) {
    413             pos = parseG1(data, pos);
    414         }
    415         return pos;
    416     }
    417 
    418     private int parseC0(byte[] data, int pos) {
    419         // For the details of C0 code group, see CEA-708B Section 7.4.1.
    420         // CL Group: C0 Subset of ASCII Control codes
    421         if (mCommand >= Cea708Data.CODE_C0_SKIP2_RANGE_START
    422                 && mCommand <= Cea708Data.CODE_C0_SKIP2_RANGE_END) {
    423             if (mCommand == Cea708Data.CODE_C0_P16) {
    424                 // TODO : P16 escapes next two bytes for the large character maps.(no standard rule)
    425                 // TODO : For korea broadcasting, express whole letters by using this.
    426                 try {
    427                     if (data[pos] == 0) {
    428                         mBuffer.append((char) data[pos + 1]);
    429                     } else {
    430                         String value = new String(Arrays.copyOfRange(data, pos, pos + 2), "EUC-KR");
    431                         mBuffer.append(value);
    432                     }
    433                 } catch (UnsupportedEncodingException e) {
    434                     Log.e(TAG, "P16 Code - Could not find supported encoding", e);
    435                 }
    436             }
    437             pos += 2;
    438         } else if (mCommand >= Cea708Data.CODE_C0_SKIP1_RANGE_START
    439                 && mCommand <= Cea708Data.CODE_C0_SKIP1_RANGE_END) {
    440             ++pos;
    441         } else {
    442             // NUL, BS, FF, CR interpreted as they are in ASCII control codes.
    443             // HCR moves the pen location to th beginning of the current line and deletes contents.
    444             // FF clears the screen and moves the pen location to (0,0).
    445             // ETX is the NULL command which is used to flush text to the current window when no
    446             // other command is pending.
    447             switch (mCommand) {
    448                 case Cea708Data.CODE_C0_NUL:
    449                     break;
    450                 case Cea708Data.CODE_C0_ETX:
    451                     emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand));
    452                     break;
    453                 case Cea708Data.CODE_C0_BS:
    454                     emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand));
    455                     break;
    456                 case Cea708Data.CODE_C0_FF:
    457                     emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand));
    458                     break;
    459                 case Cea708Data.CODE_C0_CR:
    460                     mBuffer.append('\n');
    461                     break;
    462                 case Cea708Data.CODE_C0_HCR:
    463                     emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand));
    464                     break;
    465                 default:
    466                     break;
    467             }
    468         }
    469         return pos;
    470     }
    471 
    472     private int parseC1(byte[] data, int pos) {
    473         // For the details of C1 code group, see CEA-708B Section 8.10.
    474         // CR Group: C1 Caption Control Codes
    475         switch (mCommand) {
    476             case Cea708Data.CODE_C1_CW0:
    477             case Cea708Data.CODE_C1_CW1:
    478             case Cea708Data.CODE_C1_CW2:
    479             case Cea708Data.CODE_C1_CW3:
    480             case Cea708Data.CODE_C1_CW4:
    481             case Cea708Data.CODE_C1_CW5:
    482             case Cea708Data.CODE_C1_CW6:
    483             case Cea708Data.CODE_C1_CW7:
    484                 {
    485                     // SetCurrentWindow0-7
    486                     int windowId = mCommand - Cea708Data.CODE_C1_CW0;
    487                     emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_CWX, windowId));
    488                     if (DEBUG) {
    489                         Log.d(TAG, String.format("CaptionCommand CWX windowId: %d", windowId));
    490                     }
    491                     break;
    492                 }
    493 
    494             case Cea708Data.CODE_C1_CLW:
    495                 {
    496                     // ClearWindows
    497                     int windowBitmap = data[pos] & 0xff;
    498                     ++pos;
    499                     emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_CLW, windowBitmap));
    500                     if (DEBUG) {
    501                         Log.d(
    502                                 TAG,
    503                                 String.format("CaptionCommand CLW windowBitmap: %d", windowBitmap));
    504                     }
    505                     break;
    506                 }
    507 
    508             case Cea708Data.CODE_C1_DSW:
    509                 {
    510                     // DisplayWindows
    511                     int windowBitmap = data[pos] & 0xff;
    512                     ++pos;
    513                     emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DSW, windowBitmap));
    514                     if (DEBUG) {
    515                         Log.d(
    516                                 TAG,
    517                                 String.format("CaptionCommand DSW windowBitmap: %d", windowBitmap));
    518                     }
    519                     break;
    520                 }
    521 
    522             case Cea708Data.CODE_C1_HDW:
    523                 {
    524                     // HideWindows
    525                     int windowBitmap = data[pos] & 0xff;
    526                     ++pos;
    527                     emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_HDW, windowBitmap));
    528                     if (DEBUG) {
    529                         Log.d(
    530                                 TAG,
    531                                 String.format("CaptionCommand HDW windowBitmap: %d", windowBitmap));
    532                     }
    533                     break;
    534                 }
    535 
    536             case Cea708Data.CODE_C1_TGW:
    537                 {
    538                     // ToggleWindows
    539                     int windowBitmap = data[pos] & 0xff;
    540                     ++pos;
    541                     emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_TGW, windowBitmap));
    542                     if (DEBUG) {
    543                         Log.d(
    544                                 TAG,
    545                                 String.format("CaptionCommand TGW windowBitmap: %d", windowBitmap));
    546                     }
    547                     break;
    548                 }
    549 
    550             case Cea708Data.CODE_C1_DLW:
    551                 {
    552                     // DeleteWindows
    553                     int windowBitmap = data[pos] & 0xff;
    554                     ++pos;
    555                     emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLW, windowBitmap));
    556                     if (DEBUG) {
    557                         Log.d(
    558                                 TAG,
    559                                 String.format("CaptionCommand DLW windowBitmap: %d", windowBitmap));
    560                     }
    561                     break;
    562                 }
    563 
    564             case Cea708Data.CODE_C1_DLY:
    565                 {
    566                     // Delay
    567                     int tenthsOfSeconds = data[pos] & 0xff;
    568                     ++pos;
    569                     emitCaptionEvent(
    570                             new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLY, tenthsOfSeconds));
    571                     if (DEBUG) {
    572                         Log.d(
    573                                 TAG,
    574                                 String.format(
    575                                         "CaptionCommand DLY %d tenths of seconds",
    576                                         tenthsOfSeconds));
    577                     }
    578                     break;
    579                 }
    580             case Cea708Data.CODE_C1_DLC:
    581                 {
    582                     // DelayCancel
    583                     emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLC, null));
    584                     if (DEBUG) {
    585                         Log.d(TAG, "CaptionCommand DLC");
    586                     }
    587                     break;
    588                 }
    589 
    590             case Cea708Data.CODE_C1_RST:
    591                 {
    592                     // Reset
    593                     emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_RST, null));
    594                     if (DEBUG) {
    595                         Log.d(TAG, "CaptionCommand RST");
    596                     }
    597                     break;
    598                 }
    599 
    600             case Cea708Data.CODE_C1_SPA:
    601                 {
    602                     // SetPenAttributes
    603                     int textTag = (data[pos] & 0xf0) >> 4;
    604                     int penSize = data[pos] & 0x03;
    605                     int penOffset = (data[pos] & 0x0c) >> 2;
    606                     boolean italic = (data[pos + 1] & 0x80) != 0;
    607                     boolean underline = (data[pos + 1] & 0x40) != 0;
    608                     int edgeType = (data[pos + 1] & 0x38) >> 3;
    609                     int fontTag = data[pos + 1] & 0x7;
    610                     pos += 2;
    611                     emitCaptionEvent(
    612                             new CaptionEvent(
    613                                     CAPTION_EMIT_TYPE_COMMAND_SPA,
    614                                     new CaptionPenAttr(
    615                                             penSize, penOffset, textTag, fontTag, edgeType,
    616                                             underline, italic)));
    617                     if (DEBUG) {
    618                         Log.d(
    619                                 TAG,
    620                                 String.format(
    621                                         "CaptionCommand SPA penSize: %d, penOffset: %d, textTag: %d, "
    622                                                 + "fontTag: %d, edgeType: %d, underline: %s, italic: %s",
    623                                         penSize, penOffset, textTag, fontTag, edgeType, underline,
    624                                         italic));
    625                     }
    626                     break;
    627                 }
    628 
    629             case Cea708Data.CODE_C1_SPC:
    630                 {
    631                     // SetPenColor
    632                     int opacity = (data[pos] & 0xc0) >> 6;
    633                     int red = (data[pos] & 0x30) >> 4;
    634                     int green = (data[pos] & 0x0c) >> 2;
    635                     int blue = data[pos] & 0x03;
    636                     CaptionColor foregroundColor = new CaptionColor(opacity, red, green, blue);
    637                     ++pos;
    638                     opacity = (data[pos] & 0xc0) >> 6;
    639                     red = (data[pos] & 0x30) >> 4;
    640                     green = (data[pos] & 0x0c) >> 2;
    641                     blue = data[pos] & 0x03;
    642                     CaptionColor backgroundColor = new CaptionColor(opacity, red, green, blue);
    643                     ++pos;
    644                     red = (data[pos] & 0x30) >> 4;
    645                     green = (data[pos] & 0x0c) >> 2;
    646                     blue = data[pos] & 0x03;
    647                     CaptionColor edgeColor =
    648                             new CaptionColor(CaptionColor.OPACITY_SOLID, red, green, blue);
    649                     ++pos;
    650                     emitCaptionEvent(
    651                             new CaptionEvent(
    652                                     CAPTION_EMIT_TYPE_COMMAND_SPC,
    653                                     new CaptionPenColor(
    654                                             foregroundColor, backgroundColor, edgeColor)));
    655                     if (DEBUG) {
    656                         Log.d(
    657                                 TAG,
    658                                 String.format(
    659                                         "CaptionCommand SPC foregroundColor %s backgroundColor %s edgeColor %s",
    660                                         foregroundColor, backgroundColor, edgeColor));
    661                     }
    662                     break;
    663                 }
    664 
    665             case Cea708Data.CODE_C1_SPL:
    666                 {
    667                     // SetPenLocation
    668                     // column is normally 0-31 for 4:3 formats, and 0-41 for 16:9 formats
    669                     int row = data[pos] & 0x0f;
    670                     int column = data[pos + 1] & 0x3f;
    671                     pos += 2;
    672                     emitCaptionEvent(
    673                             new CaptionEvent(
    674                                     CAPTION_EMIT_TYPE_COMMAND_SPL,
    675                                     new CaptionPenLocation(row, column)));
    676                     if (DEBUG) {
    677                         Log.d(
    678                                 TAG,
    679                                 String.format(
    680                                         "CaptionCommand SPL row: %d, column: %d", row, column));
    681                     }
    682                     break;
    683                 }
    684 
    685             case Cea708Data.CODE_C1_SWA:
    686                 {
    687                     // SetWindowAttributes
    688                     int opacity = (data[pos] & 0xc0) >> 6;
    689                     int red = (data[pos] & 0x30) >> 4;
    690                     int green = (data[pos] & 0x0c) >> 2;
    691                     int blue = data[pos] & 0x03;
    692                     CaptionColor fillColor = new CaptionColor(opacity, red, green, blue);
    693                     int borderType = (data[pos + 1] & 0xc0) >> 6 | (data[pos + 2] & 0x80) >> 5;
    694                     red = (data[pos + 1] & 0x30) >> 4;
    695                     green = (data[pos + 1] & 0x0c) >> 2;
    696                     blue = data[pos + 1] & 0x03;
    697                     CaptionColor borderColor =
    698                             new CaptionColor(CaptionColor.OPACITY_SOLID, red, green, blue);
    699                     boolean wordWrap = (data[pos + 2] & 0x40) != 0;
    700                     int printDirection = (data[pos + 2] & 0x30) >> 4;
    701                     int scrollDirection = (data[pos + 2] & 0x0c) >> 2;
    702                     int justify = (data[pos + 2] & 0x03);
    703                     int effectSpeed = (data[pos + 3] & 0xf0) >> 4;
    704                     int effectDirection = (data[pos + 3] & 0x0c) >> 2;
    705                     int displayEffect = data[pos + 3] & 0x3;
    706                     pos += 4;
    707                     emitCaptionEvent(
    708                             new CaptionEvent(
    709                                     CAPTION_EMIT_TYPE_COMMAND_SWA,
    710                                     new CaptionWindowAttr(
    711                                             fillColor,
    712                                             borderColor,
    713                                             borderType,
    714                                             wordWrap,
    715                                             printDirection,
    716                                             scrollDirection,
    717                                             justify,
    718                                             effectDirection,
    719                                             effectSpeed,
    720                                             displayEffect)));
    721                     if (DEBUG) {
    722                         Log.d(
    723                                 TAG,
    724                                 String.format(
    725                                         "CaptionCommand SWA fillColor: %s, borderColor: %s, borderType: %d"
    726                                                 + "wordWrap: %s, printDirection: %d, scrollDirection: %d, "
    727                                                 + "justify: %s, effectDirection: %d, effectSpeed: %d, "
    728                                                 + "displayEffect: %d",
    729                                         fillColor,
    730                                         borderColor,
    731                                         borderType,
    732                                         wordWrap,
    733                                         printDirection,
    734                                         scrollDirection,
    735                                         justify,
    736                                         effectDirection,
    737                                         effectSpeed,
    738                                         displayEffect));
    739                     }
    740                     break;
    741                 }
    742 
    743             case Cea708Data.CODE_C1_DF0:
    744             case Cea708Data.CODE_C1_DF1:
    745             case Cea708Data.CODE_C1_DF2:
    746             case Cea708Data.CODE_C1_DF3:
    747             case Cea708Data.CODE_C1_DF4:
    748             case Cea708Data.CODE_C1_DF5:
    749             case Cea708Data.CODE_C1_DF6:
    750             case Cea708Data.CODE_C1_DF7:
    751                 {
    752                     // DefineWindow0-7
    753                     int windowId = mCommand - Cea708Data.CODE_C1_DF0;
    754                     boolean visible = (data[pos] & 0x20) != 0;
    755                     boolean rowLock = (data[pos] & 0x10) != 0;
    756                     boolean columnLock = (data[pos] & 0x08) != 0;
    757                     int priority = data[pos] & 0x07;
    758                     boolean relativePositioning = (data[pos + 1] & 0x80) != 0;
    759                     int anchorVertical = data[pos + 1] & 0x7f;
    760                     int anchorHorizontal = data[pos + 2] & 0xff;
    761                     int anchorId = (data[pos + 3] & 0xf0) >> 4;
    762                     int rowCount = data[pos + 3] & 0x0f;
    763                     int columnCount = data[pos + 4] & 0x3f;
    764                     int windowStyle = (data[pos + 5] & 0x38) >> 3;
    765                     int penStyle = data[pos + 5] & 0x07;
    766                     pos += 6;
    767                     emitCaptionEvent(
    768                             new CaptionEvent(
    769                                     CAPTION_EMIT_TYPE_COMMAND_DFX,
    770                                     new CaptionWindow(
    771                                             windowId,
    772                                             visible,
    773                                             rowLock,
    774                                             columnLock,
    775                                             priority,
    776                                             relativePositioning,
    777                                             anchorVertical,
    778                                             anchorHorizontal,
    779                                             anchorId,
    780                                             rowCount,
    781                                             columnCount,
    782                                             penStyle,
    783                                             windowStyle)));
    784                     if (DEBUG) {
    785                         Log.d(
    786                                 TAG,
    787                                 String.format(
    788                                         "CaptionCommand DFx windowId: %d, priority: %d, columnLock: %s, "
    789                                                 + "rowLock: %s, visible: %s, anchorVertical: %d, "
    790                                                 + "relativePositioning: %s, anchorHorizontal: %d, "
    791                                                 + "rowCount: %d, anchorId: %d, columnCount: %d, penStyle: %d, "
    792                                                 + "windowStyle: %d",
    793                                         windowId,
    794                                         priority,
    795                                         columnLock,
    796                                         rowLock,
    797                                         visible,
    798                                         anchorVertical,
    799                                         relativePositioning,
    800                                         anchorHorizontal,
    801                                         rowCount,
    802                                         anchorId,
    803                                         columnCount,
    804                                         penStyle,
    805                                         windowStyle));
    806                     }
    807                     break;
    808                 }
    809 
    810             default:
    811                 break;
    812         }
    813         return pos;
    814     }
    815 
    816     private int parseG0(byte[] data, int pos) {
    817         // For the details of G0 code group, see CEA-708B Section 7.4.3.
    818         // GL Group: G0 Modified version of ANSI X3.4 Printable Character Set (ASCII)
    819         if (mCommand == Cea708Data.CODE_G0_MUSICNOTE) {
    820             // Music note.
    821             mBuffer.append(MUSIC_NOTE_CHAR);
    822         } else {
    823             // Put ASCII code into buffer.
    824             mBuffer.append((char) mCommand);
    825         }
    826         return pos;
    827     }
    828 
    829     private int parseG1(byte[] data, int pos) {
    830         // For the details of G0 code group, see CEA-708B Section 7.4.4.
    831         // GR Group: G1 ISO 8859-1 Latin 1 Characters
    832         // Put ASCII Extended character set into buffer.
    833         mBuffer.append((char) mCommand);
    834         return pos;
    835     }
    836 
    837     // Step 4. Extended code groups
    838     private int parseExt1(byte[] data, int pos) {
    839         // For the details of EXT1 code group, see CEA-708B Section 7.2.
    840         mCommand = data[pos] & 0xff;
    841         ++pos;
    842         if (mCommand >= Cea708Data.CODE_C2_RANGE_START
    843                 && mCommand <= Cea708Data.CODE_C2_RANGE_END) {
    844             pos = parseC2(data, pos);
    845         } else if (mCommand >= Cea708Data.CODE_C3_RANGE_START
    846                 && mCommand <= Cea708Data.CODE_C3_RANGE_END) {
    847             pos = parseC3(data, pos);
    848         } else if (mCommand >= Cea708Data.CODE_G2_RANGE_START
    849                 && mCommand <= Cea708Data.CODE_G2_RANGE_END) {
    850             pos = parseG2(data, pos);
    851         } else if (mCommand >= Cea708Data.CODE_G3_RANGE_START
    852                 && mCommand <= Cea708Data.CODE_G3_RANGE_END) {
    853             pos = parseG3(data, pos);
    854         }
    855         return pos;
    856     }
    857 
    858     private int parseC2(byte[] data, int pos) {
    859         // For the details of C2 code group, see CEA-708B Section 7.4.7.
    860         // Extended Miscellaneous Control Codes
    861         // C2 Table : No commands as of CEA-708B. A decoder must skip.
    862         if (mCommand >= Cea708Data.CODE_C2_SKIP0_RANGE_START
    863                 && mCommand <= Cea708Data.CODE_C2_SKIP0_RANGE_END) {
    864             // Do nothing.
    865         } else if (mCommand >= Cea708Data.CODE_C2_SKIP1_RANGE_START
    866                 && mCommand <= Cea708Data.CODE_C2_SKIP1_RANGE_END) {
    867             ++pos;
    868         } else if (mCommand >= Cea708Data.CODE_C2_SKIP2_RANGE_START
    869                 && mCommand <= Cea708Data.CODE_C2_SKIP2_RANGE_END) {
    870             pos += 2;
    871         } else if (mCommand >= Cea708Data.CODE_C2_SKIP3_RANGE_START
    872                 && mCommand <= Cea708Data.CODE_C2_SKIP3_RANGE_END) {
    873             pos += 3;
    874         }
    875         return pos;
    876     }
    877 
    878     private int parseC3(byte[] data, int pos) {
    879         // For the details of C3 code group, see CEA-708B Section 7.4.8.
    880         // Extended Control Code Set 2
    881         // C3 Table : No commands as of CEA-708B. A decoder must skip.
    882         if (mCommand >= Cea708Data.CODE_C3_SKIP4_RANGE_START
    883                 && mCommand <= Cea708Data.CODE_C3_SKIP4_RANGE_END) {
    884             pos += 4;
    885         } else if (mCommand >= Cea708Data.CODE_C3_SKIP5_RANGE_START
    886                 && mCommand <= Cea708Data.CODE_C3_SKIP5_RANGE_END) {
    887             pos += 5;
    888         }
    889         return pos;
    890     }
    891 
    892     private int parseG2(byte[] data, int pos) {
    893         // For the details of C3 code group, see CEA-708B Section 7.4.5.
    894         // Extended Control Code Set 1(G2 Table)
    895         switch (mCommand) {
    896             case Cea708Data.CODE_G2_TSP:
    897                 // TODO : TSP is the Transparent space
    898                 break;
    899             case Cea708Data.CODE_G2_NBTSP:
    900                 // TODO : NBTSP is Non-Breaking Transparent Space.
    901                 break;
    902             case Cea708Data.CODE_G2_BLK:
    903                 // TODO : BLK indicates a solid block which fills the entire character block
    904                 // TODO : with a solid foreground color.
    905                 break;
    906             default:
    907                 break;
    908         }
    909         return pos;
    910     }
    911 
    912     private int parseG3(byte[] data, int pos) {
    913         // For the details of C3 code group, see CEA-708B Section 7.4.6.
    914         // Future characters and icons(G3 Table)
    915         if (mCommand == Cea708Data.CODE_G3_CC) {
    916             // TODO : [CC] icon with square corners
    917         }
    918 
    919         // Do nothing
    920         return pos;
    921     }
    922 }
    923