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 -> 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 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