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