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