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