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