1 /* 2 * Copyright (C) 2016 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 package com.google.android.exoplayer2.text.cea; 17 18 import android.graphics.Color; 19 import android.graphics.Typeface; 20 import android.text.Layout.Alignment; 21 import android.text.SpannableString; 22 import android.text.SpannableStringBuilder; 23 import android.text.Spanned; 24 import android.text.style.BackgroundColorSpan; 25 import android.text.style.ForegroundColorSpan; 26 import android.text.style.StyleSpan; 27 import android.text.style.UnderlineSpan; 28 import androidx.annotation.Nullable; 29 import com.google.android.exoplayer2.C; 30 import com.google.android.exoplayer2.Format; 31 import com.google.android.exoplayer2.text.Cue; 32 import com.google.android.exoplayer2.text.Cue.AnchorType; 33 import com.google.android.exoplayer2.text.Subtitle; 34 import com.google.android.exoplayer2.text.SubtitleDecoder; 35 import com.google.android.exoplayer2.text.SubtitleInputBuffer; 36 import com.google.android.exoplayer2.util.Assertions; 37 import com.google.android.exoplayer2.util.CodecSpecificDataUtil; 38 import com.google.android.exoplayer2.util.Log; 39 import com.google.android.exoplayer2.util.ParsableBitArray; 40 import com.google.android.exoplayer2.util.ParsableByteArray; 41 import java.nio.ByteBuffer; 42 import java.util.ArrayList; 43 import java.util.Collections; 44 import java.util.List; 45 import org.checkerframework.checker.nullness.qual.RequiresNonNull; 46 47 /** 48 * A {@link SubtitleDecoder} for CEA-708 (also known as "EIA-708"). 49 */ 50 public final class Cea708Decoder extends CeaDecoder { 51 52 private static final String TAG = "Cea708Decoder"; 53 54 private static final int NUM_WINDOWS = 8; 55 56 private static final int DTVCC_PACKET_DATA = 0x02; 57 private static final int DTVCC_PACKET_START = 0x03; 58 private static final int CC_VALID_FLAG = 0x04; 59 60 // Base Commands 61 private static final int GROUP_C0_END = 0x1F; // Miscellaneous Control Codes 62 private static final int GROUP_G0_END = 0x7F; // ASCII Printable Characters 63 private static final int GROUP_C1_END = 0x9F; // Captioning Command Control Codes 64 private static final int GROUP_G1_END = 0xFF; // ISO 8859-1 LATIN-1 Character Set 65 66 // Extended Commands 67 private static final int GROUP_C2_END = 0x1F; // Extended Control Code Set 1 68 private static final int GROUP_G2_END = 0x7F; // Extended Miscellaneous Characters 69 private static final int GROUP_C3_END = 0x9F; // Extended Control Code Set 2 70 private static final int GROUP_G3_END = 0xFF; // Future Expansion 71 72 // Group C0 Commands 73 private static final int COMMAND_NUL = 0x00; // Nul 74 private static final int COMMAND_ETX = 0x03; // EndOfText 75 private static final int COMMAND_BS = 0x08; // Backspace 76 private static final int COMMAND_FF = 0x0C; // FormFeed (Flush) 77 private static final int COMMAND_CR = 0x0D; // CarriageReturn 78 private static final int COMMAND_HCR = 0x0E; // ClearLine 79 private static final int COMMAND_EXT1 = 0x10; // Extended Control Code Flag 80 private static final int COMMAND_EXT1_START = 0x11; 81 private static final int COMMAND_EXT1_END = 0x17; 82 private static final int COMMAND_P16_START = 0x18; 83 private static final int COMMAND_P16_END = 0x1F; 84 85 // Group C1 Commands 86 private static final int COMMAND_CW0 = 0x80; // SetCurrentWindow to 0 87 private static final int COMMAND_CW1 = 0x81; // SetCurrentWindow to 1 88 private static final int COMMAND_CW2 = 0x82; // SetCurrentWindow to 2 89 private static final int COMMAND_CW3 = 0x83; // SetCurrentWindow to 3 90 private static final int COMMAND_CW4 = 0x84; // SetCurrentWindow to 4 91 private static final int COMMAND_CW5 = 0x85; // SetCurrentWindow to 5 92 private static final int COMMAND_CW6 = 0x86; // SetCurrentWindow to 6 93 private static final int COMMAND_CW7 = 0x87; // SetCurrentWindow to 7 94 private static final int COMMAND_CLW = 0x88; // ClearWindows (+1 byte) 95 private static final int COMMAND_DSW = 0x89; // DisplayWindows (+1 byte) 96 private static final int COMMAND_HDW = 0x8A; // HideWindows (+1 byte) 97 private static final int COMMAND_TGW = 0x8B; // ToggleWindows (+1 byte) 98 private static final int COMMAND_DLW = 0x8C; // DeleteWindows (+1 byte) 99 private static final int COMMAND_DLY = 0x8D; // Delay (+1 byte) 100 private static final int COMMAND_DLC = 0x8E; // DelayCancel 101 private static final int COMMAND_RST = 0x8F; // Reset 102 private static final int COMMAND_SPA = 0x90; // SetPenAttributes (+2 bytes) 103 private static final int COMMAND_SPC = 0x91; // SetPenColor (+3 bytes) 104 private static final int COMMAND_SPL = 0x92; // SetPenLocation (+2 bytes) 105 private static final int COMMAND_SWA = 0x97; // SetWindowAttributes (+4 bytes) 106 private static final int COMMAND_DF0 = 0x98; // DefineWindow 0 (+6 bytes) 107 private static final int COMMAND_DF1 = 0x99; // DefineWindow 1 (+6 bytes) 108 private static final int COMMAND_DF2 = 0x9A; // DefineWindow 2 (+6 bytes) 109 private static final int COMMAND_DF3 = 0x9B; // DefineWindow 3 (+6 bytes) 110 private static final int COMMAND_DF4 = 0x9C; // DefineWindow 4 (+6 bytes) 111 private static final int COMMAND_DF5 = 0x9D; // DefineWindow 5 (+6 bytes) 112 private static final int COMMAND_DF6 = 0x9E; // DefineWindow 6 (+6 bytes) 113 private static final int COMMAND_DF7 = 0x9F; // DefineWindow 7 (+6 bytes) 114 115 // G0 Table Special Chars 116 private static final int CHARACTER_MN = 0x7F; // MusicNote 117 118 // G2 Table Special Chars 119 private static final int CHARACTER_TSP = 0x20; 120 private static final int CHARACTER_NBTSP = 0x21; 121 private static final int CHARACTER_ELLIPSIS = 0x25; 122 private static final int CHARACTER_BIG_CARONS = 0x2A; 123 private static final int CHARACTER_BIG_OE = 0x2C; 124 private static final int CHARACTER_SOLID_BLOCK = 0x30; 125 private static final int CHARACTER_OPEN_SINGLE_QUOTE = 0x31; 126 private static final int CHARACTER_CLOSE_SINGLE_QUOTE = 0x32; 127 private static final int CHARACTER_OPEN_DOUBLE_QUOTE = 0x33; 128 private static final int CHARACTER_CLOSE_DOUBLE_QUOTE = 0x34; 129 private static final int CHARACTER_BOLD_BULLET = 0x35; 130 private static final int CHARACTER_TM = 0x39; 131 private static final int CHARACTER_SMALL_CARONS = 0x3A; 132 private static final int CHARACTER_SMALL_OE = 0x3C; 133 private static final int CHARACTER_SM = 0x3D; 134 private static final int CHARACTER_DIAERESIS_Y = 0x3F; 135 private static final int CHARACTER_ONE_EIGHTH = 0x76; 136 private static final int CHARACTER_THREE_EIGHTHS = 0x77; 137 private static final int CHARACTER_FIVE_EIGHTHS = 0x78; 138 private static final int CHARACTER_SEVEN_EIGHTHS = 0x79; 139 private static final int CHARACTER_VERTICAL_BORDER = 0x7A; 140 private static final int CHARACTER_UPPER_RIGHT_BORDER = 0x7B; 141 private static final int CHARACTER_LOWER_LEFT_BORDER = 0x7C; 142 private static final int CHARACTER_HORIZONTAL_BORDER = 0x7D; 143 private static final int CHARACTER_LOWER_RIGHT_BORDER = 0x7E; 144 private static final int CHARACTER_UPPER_LEFT_BORDER = 0x7F; 145 146 private final ParsableByteArray ccData; 147 private final ParsableBitArray serviceBlockPacket; 148 // TODO: Use isWideAspectRatio in decoding. 149 @SuppressWarnings({"unused", "FieldCanBeLocal"}) 150 private final boolean isWideAspectRatio; 151 152 private final int selectedServiceNumber; 153 private final CueInfoBuilder[] cueInfoBuilders; 154 155 private CueInfoBuilder currentCueInfoBuilder; 156 @Nullable private List<Cue> cues; 157 @Nullable private List<Cue> lastCues; 158 159 @Nullable private DtvCcPacket currentDtvCcPacket; 160 private int currentWindow; 161 Cea708Decoder(int accessibilityChannel, @Nullable List<byte[]> initializationData)162 public Cea708Decoder(int accessibilityChannel, @Nullable List<byte[]> initializationData) { 163 ccData = new ParsableByteArray(); 164 serviceBlockPacket = new ParsableBitArray(); 165 selectedServiceNumber = accessibilityChannel == Format.NO_VALUE ? 1 : accessibilityChannel; 166 isWideAspectRatio = 167 initializationData != null 168 && CodecSpecificDataUtil.parseCea708InitializationData(initializationData); 169 170 cueInfoBuilders = new CueInfoBuilder[NUM_WINDOWS]; 171 for (int i = 0; i < NUM_WINDOWS; i++) { 172 cueInfoBuilders[i] = new CueInfoBuilder(); 173 } 174 175 currentCueInfoBuilder = cueInfoBuilders[0]; 176 } 177 178 @Override getName()179 public String getName() { 180 return "Cea708Decoder"; 181 } 182 183 @Override flush()184 public void flush() { 185 super.flush(); 186 cues = null; 187 lastCues = null; 188 currentWindow = 0; 189 currentCueInfoBuilder = cueInfoBuilders[currentWindow]; 190 resetCueBuilders(); 191 currentDtvCcPacket = null; 192 } 193 194 @Override isNewSubtitleDataAvailable()195 protected boolean isNewSubtitleDataAvailable() { 196 return cues != lastCues; 197 } 198 199 @Override createSubtitle()200 protected Subtitle createSubtitle() { 201 lastCues = cues; 202 return new CeaSubtitle(Assertions.checkNotNull(cues)); 203 } 204 205 @Override decode(SubtitleInputBuffer inputBuffer)206 protected void decode(SubtitleInputBuffer inputBuffer) { 207 // Subtitle input buffers are non-direct and the position is zero, so calling array() is safe. 208 ByteBuffer subtitleData = Assertions.checkNotNull(inputBuffer.data); 209 @SuppressWarnings("ByteBufferBackingArray") 210 byte[] inputBufferData = subtitleData.array(); 211 ccData.reset(inputBufferData, subtitleData.limit()); 212 while (ccData.bytesLeft() >= 3) { 213 int ccTypeAndValid = (ccData.readUnsignedByte() & 0x07); 214 215 int ccType = ccTypeAndValid & (DTVCC_PACKET_DATA | DTVCC_PACKET_START); 216 boolean ccValid = (ccTypeAndValid & CC_VALID_FLAG) == CC_VALID_FLAG; 217 byte ccData1 = (byte) ccData.readUnsignedByte(); 218 byte ccData2 = (byte) ccData.readUnsignedByte(); 219 220 // Ignore any non-CEA-708 data 221 if (ccType != DTVCC_PACKET_DATA && ccType != DTVCC_PACKET_START) { 222 continue; 223 } 224 225 if (!ccValid) { 226 // This byte-pair isn't valid, ignore it and continue. 227 continue; 228 } 229 230 if (ccType == DTVCC_PACKET_START) { 231 finalizeCurrentPacket(); 232 233 int sequenceNumber = (ccData1 & 0xC0) >> 6; // first 2 bits 234 int packetSize = ccData1 & 0x3F; // last 6 bits 235 if (packetSize == 0) { 236 packetSize = 64; 237 } 238 239 currentDtvCcPacket = new DtvCcPacket(sequenceNumber, packetSize); 240 currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2; 241 } else { 242 // The only remaining valid packet type is DTVCC_PACKET_DATA 243 Assertions.checkArgument(ccType == DTVCC_PACKET_DATA); 244 245 if (currentDtvCcPacket == null) { 246 Log.e(TAG, "Encountered DTVCC_PACKET_DATA before DTVCC_PACKET_START"); 247 continue; 248 } 249 250 currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData1; 251 currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2; 252 } 253 254 if (currentDtvCcPacket.currentIndex == (currentDtvCcPacket.packetSize * 2 - 1)) { 255 finalizeCurrentPacket(); 256 } 257 } 258 } 259 finalizeCurrentPacket()260 private void finalizeCurrentPacket() { 261 if (currentDtvCcPacket == null) { 262 // No packet to finalize; 263 return; 264 } 265 266 processCurrentPacket(); 267 currentDtvCcPacket = null; 268 } 269 270 @RequiresNonNull("currentDtvCcPacket") processCurrentPacket()271 private void processCurrentPacket() { 272 if (currentDtvCcPacket.currentIndex != (currentDtvCcPacket.packetSize * 2 - 1)) { 273 Log.w(TAG, "DtvCcPacket ended prematurely; size is " + (currentDtvCcPacket.packetSize * 2 - 1) 274 + ", but current index is " + currentDtvCcPacket.currentIndex + " (sequence number " 275 + currentDtvCcPacket.sequenceNumber + "); ignoring packet"); 276 return; 277 } 278 279 serviceBlockPacket.reset(currentDtvCcPacket.packetData, currentDtvCcPacket.currentIndex); 280 281 int serviceNumber = serviceBlockPacket.readBits(3); 282 int blockSize = serviceBlockPacket.readBits(5); 283 if (serviceNumber == 7) { 284 // extended service numbers 285 serviceBlockPacket.skipBits(2); 286 serviceNumber = serviceBlockPacket.readBits(6); 287 if (serviceNumber < 7) { 288 Log.w(TAG, "Invalid extended service number: " + serviceNumber); 289 } 290 } 291 292 // Ignore packets in which blockSize is 0 293 if (blockSize == 0) { 294 if (serviceNumber != 0) { 295 Log.w(TAG, "serviceNumber is non-zero (" + serviceNumber + ") when blockSize is 0"); 296 } 297 return; 298 } 299 300 if (serviceNumber != selectedServiceNumber) { 301 return; 302 } 303 304 // The cues should be updated if we receive a C0 ETX command, any C1 command, or if after 305 // processing the service block any text has been added to the buffer. See CEA-708-B Section 306 // 8.10.4 for more details. 307 boolean cuesNeedUpdate = false; 308 309 while (serviceBlockPacket.bitsLeft() > 0) { 310 int command = serviceBlockPacket.readBits(8); 311 if (command != COMMAND_EXT1) { 312 if (command <= GROUP_C0_END) { 313 handleC0Command(command); 314 // If the C0 command was an ETX command, the cues are updated in handleC0Command. 315 } else if (command <= GROUP_G0_END) { 316 handleG0Character(command); 317 cuesNeedUpdate = true; 318 } else if (command <= GROUP_C1_END) { 319 handleC1Command(command); 320 cuesNeedUpdate = true; 321 } else if (command <= GROUP_G1_END) { 322 handleG1Character(command); 323 cuesNeedUpdate = true; 324 } else { 325 Log.w(TAG, "Invalid base command: " + command); 326 } 327 } else { 328 // Read the extended command 329 command = serviceBlockPacket.readBits(8); 330 if (command <= GROUP_C2_END) { 331 handleC2Command(command); 332 } else if (command <= GROUP_G2_END) { 333 handleG2Character(command); 334 cuesNeedUpdate = true; 335 } else if (command <= GROUP_C3_END) { 336 handleC3Command(command); 337 } else if (command <= GROUP_G3_END) { 338 handleG3Character(command); 339 cuesNeedUpdate = true; 340 } else { 341 Log.w(TAG, "Invalid extended command: " + command); 342 } 343 } 344 } 345 346 if (cuesNeedUpdate) { 347 cues = getDisplayCues(); 348 } 349 } 350 handleC0Command(int command)351 private void handleC0Command(int command) { 352 switch (command) { 353 case COMMAND_NUL: 354 // Do nothing. 355 break; 356 case COMMAND_ETX: 357 cues = getDisplayCues(); 358 break; 359 case COMMAND_BS: 360 currentCueInfoBuilder.backspace(); 361 break; 362 case COMMAND_FF: 363 resetCueBuilders(); 364 break; 365 case COMMAND_CR: 366 currentCueInfoBuilder.append('\n'); 367 break; 368 case COMMAND_HCR: 369 // TODO: Add support for this command. 370 break; 371 default: 372 if (command >= COMMAND_EXT1_START && command <= COMMAND_EXT1_END) { 373 Log.w(TAG, "Currently unsupported COMMAND_EXT1 Command: " + command); 374 serviceBlockPacket.skipBits(8); 375 } else if (command >= COMMAND_P16_START && command <= COMMAND_P16_END) { 376 Log.w(TAG, "Currently unsupported COMMAND_P16 Command: " + command); 377 serviceBlockPacket.skipBits(16); 378 } else { 379 Log.w(TAG, "Invalid C0 command: " + command); 380 } 381 } 382 } 383 handleC1Command(int command)384 private void handleC1Command(int command) { 385 int window; 386 switch (command) { 387 case COMMAND_CW0: 388 case COMMAND_CW1: 389 case COMMAND_CW2: 390 case COMMAND_CW3: 391 case COMMAND_CW4: 392 case COMMAND_CW5: 393 case COMMAND_CW6: 394 case COMMAND_CW7: 395 window = (command - COMMAND_CW0); 396 if (currentWindow != window) { 397 currentWindow = window; 398 currentCueInfoBuilder = cueInfoBuilders[window]; 399 } 400 break; 401 case COMMAND_CLW: 402 for (int i = 1; i <= NUM_WINDOWS; i++) { 403 if (serviceBlockPacket.readBit()) { 404 cueInfoBuilders[NUM_WINDOWS - i].clear(); 405 } 406 } 407 break; 408 case COMMAND_DSW: 409 for (int i = 1; i <= NUM_WINDOWS; i++) { 410 if (serviceBlockPacket.readBit()) { 411 cueInfoBuilders[NUM_WINDOWS - i].setVisibility(true); 412 } 413 } 414 break; 415 case COMMAND_HDW: 416 for (int i = 1; i <= NUM_WINDOWS; i++) { 417 if (serviceBlockPacket.readBit()) { 418 cueInfoBuilders[NUM_WINDOWS - i].setVisibility(false); 419 } 420 } 421 break; 422 case COMMAND_TGW: 423 for (int i = 1; i <= NUM_WINDOWS; i++) { 424 if (serviceBlockPacket.readBit()) { 425 CueInfoBuilder cueInfoBuilder = cueInfoBuilders[NUM_WINDOWS - i]; 426 cueInfoBuilder.setVisibility(!cueInfoBuilder.isVisible()); 427 } 428 } 429 break; 430 case COMMAND_DLW: 431 for (int i = 1; i <= NUM_WINDOWS; i++) { 432 if (serviceBlockPacket.readBit()) { 433 cueInfoBuilders[NUM_WINDOWS - i].reset(); 434 } 435 } 436 break; 437 case COMMAND_DLY: 438 // TODO: Add support for delay commands. 439 serviceBlockPacket.skipBits(8); 440 break; 441 case COMMAND_DLC: 442 // TODO: Add support for delay commands. 443 break; 444 case COMMAND_RST: 445 resetCueBuilders(); 446 break; 447 case COMMAND_SPA: 448 if (!currentCueInfoBuilder.isDefined()) { 449 // ignore this command if the current window/cue isn't defined 450 serviceBlockPacket.skipBits(16); 451 } else { 452 handleSetPenAttributes(); 453 } 454 break; 455 case COMMAND_SPC: 456 if (!currentCueInfoBuilder.isDefined()) { 457 // ignore this command if the current window/cue isn't defined 458 serviceBlockPacket.skipBits(24); 459 } else { 460 handleSetPenColor(); 461 } 462 break; 463 case COMMAND_SPL: 464 if (!currentCueInfoBuilder.isDefined()) { 465 // ignore this command if the current window/cue isn't defined 466 serviceBlockPacket.skipBits(16); 467 } else { 468 handleSetPenLocation(); 469 } 470 break; 471 case COMMAND_SWA: 472 if (!currentCueInfoBuilder.isDefined()) { 473 // ignore this command if the current window/cue isn't defined 474 serviceBlockPacket.skipBits(32); 475 } else { 476 handleSetWindowAttributes(); 477 } 478 break; 479 case COMMAND_DF0: 480 case COMMAND_DF1: 481 case COMMAND_DF2: 482 case COMMAND_DF3: 483 case COMMAND_DF4: 484 case COMMAND_DF5: 485 case COMMAND_DF6: 486 case COMMAND_DF7: 487 window = (command - COMMAND_DF0); 488 handleDefineWindow(window); 489 // We also set the current window to the newly defined window. 490 if (currentWindow != window) { 491 currentWindow = window; 492 currentCueInfoBuilder = cueInfoBuilders[window]; 493 } 494 break; 495 default: 496 Log.w(TAG, "Invalid C1 command: " + command); 497 } 498 } 499 handleC2Command(int command)500 private void handleC2Command(int command) { 501 // C2 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes 502 if (command <= 0x07) { 503 // Do nothing. 504 } else if (command <= 0x0F) { 505 serviceBlockPacket.skipBits(8); 506 } else if (command <= 0x17) { 507 serviceBlockPacket.skipBits(16); 508 } else if (command <= 0x1F) { 509 serviceBlockPacket.skipBits(24); 510 } 511 } 512 handleC3Command(int command)513 private void handleC3Command(int command) { 514 // C3 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes 515 if (command <= 0x87) { 516 serviceBlockPacket.skipBits(32); 517 } else if (command <= 0x8F) { 518 serviceBlockPacket.skipBits(40); 519 } else if (command <= 0x9F) { 520 // 90-9F are variable length codes; the first byte defines the header with the first 521 // 2 bits specifying the type and the last 6 bits specifying the remaining length of the 522 // command in bytes 523 serviceBlockPacket.skipBits(2); 524 int length = serviceBlockPacket.readBits(6); 525 serviceBlockPacket.skipBits(8 * length); 526 } 527 } 528 handleG0Character(int characterCode)529 private void handleG0Character(int characterCode) { 530 if (characterCode == CHARACTER_MN) { 531 currentCueInfoBuilder.append('\u266B'); 532 } else { 533 currentCueInfoBuilder.append((char) (characterCode & 0xFF)); 534 } 535 } 536 handleG1Character(int characterCode)537 private void handleG1Character(int characterCode) { 538 currentCueInfoBuilder.append((char) (characterCode & 0xFF)); 539 } 540 handleG2Character(int characterCode)541 private void handleG2Character(int characterCode) { 542 switch (characterCode) { 543 case CHARACTER_TSP: 544 currentCueInfoBuilder.append('\u0020'); 545 break; 546 case CHARACTER_NBTSP: 547 currentCueInfoBuilder.append('\u00A0'); 548 break; 549 case CHARACTER_ELLIPSIS: 550 currentCueInfoBuilder.append('\u2026'); 551 break; 552 case CHARACTER_BIG_CARONS: 553 currentCueInfoBuilder.append('\u0160'); 554 break; 555 case CHARACTER_BIG_OE: 556 currentCueInfoBuilder.append('\u0152'); 557 break; 558 case CHARACTER_SOLID_BLOCK: 559 currentCueInfoBuilder.append('\u2588'); 560 break; 561 case CHARACTER_OPEN_SINGLE_QUOTE: 562 currentCueInfoBuilder.append('\u2018'); 563 break; 564 case CHARACTER_CLOSE_SINGLE_QUOTE: 565 currentCueInfoBuilder.append('\u2019'); 566 break; 567 case CHARACTER_OPEN_DOUBLE_QUOTE: 568 currentCueInfoBuilder.append('\u201C'); 569 break; 570 case CHARACTER_CLOSE_DOUBLE_QUOTE: 571 currentCueInfoBuilder.append('\u201D'); 572 break; 573 case CHARACTER_BOLD_BULLET: 574 currentCueInfoBuilder.append('\u2022'); 575 break; 576 case CHARACTER_TM: 577 currentCueInfoBuilder.append('\u2122'); 578 break; 579 case CHARACTER_SMALL_CARONS: 580 currentCueInfoBuilder.append('\u0161'); 581 break; 582 case CHARACTER_SMALL_OE: 583 currentCueInfoBuilder.append('\u0153'); 584 break; 585 case CHARACTER_SM: 586 currentCueInfoBuilder.append('\u2120'); 587 break; 588 case CHARACTER_DIAERESIS_Y: 589 currentCueInfoBuilder.append('\u0178'); 590 break; 591 case CHARACTER_ONE_EIGHTH: 592 currentCueInfoBuilder.append('\u215B'); 593 break; 594 case CHARACTER_THREE_EIGHTHS: 595 currentCueInfoBuilder.append('\u215C'); 596 break; 597 case CHARACTER_FIVE_EIGHTHS: 598 currentCueInfoBuilder.append('\u215D'); 599 break; 600 case CHARACTER_SEVEN_EIGHTHS: 601 currentCueInfoBuilder.append('\u215E'); 602 break; 603 case CHARACTER_VERTICAL_BORDER: 604 currentCueInfoBuilder.append('\u2502'); 605 break; 606 case CHARACTER_UPPER_RIGHT_BORDER: 607 currentCueInfoBuilder.append('\u2510'); 608 break; 609 case CHARACTER_LOWER_LEFT_BORDER: 610 currentCueInfoBuilder.append('\u2514'); 611 break; 612 case CHARACTER_HORIZONTAL_BORDER: 613 currentCueInfoBuilder.append('\u2500'); 614 break; 615 case CHARACTER_LOWER_RIGHT_BORDER: 616 currentCueInfoBuilder.append('\u2518'); 617 break; 618 case CHARACTER_UPPER_LEFT_BORDER: 619 currentCueInfoBuilder.append('\u250C'); 620 break; 621 default: 622 Log.w(TAG, "Invalid G2 character: " + characterCode); 623 // The CEA-708 specification doesn't specify what to do in the case of an unexpected 624 // value in the G2 character range, so we ignore it. 625 } 626 } 627 handleG3Character(int characterCode)628 private void handleG3Character(int characterCode) { 629 if (characterCode == 0xA0) { 630 currentCueInfoBuilder.append('\u33C4'); 631 } else { 632 Log.w(TAG, "Invalid G3 character: " + characterCode); 633 // Substitute any unsupported G3 character with an underscore as per CEA-708 specification. 634 currentCueInfoBuilder.append('_'); 635 } 636 } 637 handleSetPenAttributes()638 private void handleSetPenAttributes() { 639 // the SetPenAttributes command contains 2 bytes of data 640 // first byte 641 int textTag = serviceBlockPacket.readBits(4); 642 int offset = serviceBlockPacket.readBits(2); 643 int penSize = serviceBlockPacket.readBits(2); 644 // second byte 645 boolean italicsToggle = serviceBlockPacket.readBit(); 646 boolean underlineToggle = serviceBlockPacket.readBit(); 647 int edgeType = serviceBlockPacket.readBits(3); 648 int fontStyle = serviceBlockPacket.readBits(3); 649 650 currentCueInfoBuilder.setPenAttributes( 651 textTag, offset, penSize, italicsToggle, underlineToggle, edgeType, fontStyle); 652 } 653 handleSetPenColor()654 private void handleSetPenColor() { 655 // the SetPenColor command contains 3 bytes of data 656 // first byte 657 int foregroundO = serviceBlockPacket.readBits(2); 658 int foregroundR = serviceBlockPacket.readBits(2); 659 int foregroundG = serviceBlockPacket.readBits(2); 660 int foregroundB = serviceBlockPacket.readBits(2); 661 int foregroundColor = 662 CueInfoBuilder.getArgbColorFromCeaColor(foregroundR, foregroundG, foregroundB, foregroundO); 663 // second byte 664 int backgroundO = serviceBlockPacket.readBits(2); 665 int backgroundR = serviceBlockPacket.readBits(2); 666 int backgroundG = serviceBlockPacket.readBits(2); 667 int backgroundB = serviceBlockPacket.readBits(2); 668 int backgroundColor = 669 CueInfoBuilder.getArgbColorFromCeaColor(backgroundR, backgroundG, backgroundB, backgroundO); 670 // third byte 671 serviceBlockPacket.skipBits(2); // null padding 672 int edgeR = serviceBlockPacket.readBits(2); 673 int edgeG = serviceBlockPacket.readBits(2); 674 int edgeB = serviceBlockPacket.readBits(2); 675 int edgeColor = CueInfoBuilder.getArgbColorFromCeaColor(edgeR, edgeG, edgeB); 676 677 currentCueInfoBuilder.setPenColor(foregroundColor, backgroundColor, edgeColor); 678 } 679 handleSetPenLocation()680 private void handleSetPenLocation() { 681 // the SetPenLocation command contains 2 bytes of data 682 // first byte 683 serviceBlockPacket.skipBits(4); 684 int row = serviceBlockPacket.readBits(4); 685 // second byte 686 serviceBlockPacket.skipBits(2); 687 int column = serviceBlockPacket.readBits(6); 688 689 currentCueInfoBuilder.setPenLocation(row, column); 690 } 691 handleSetWindowAttributes()692 private void handleSetWindowAttributes() { 693 // the SetWindowAttributes command contains 4 bytes of data 694 // first byte 695 int fillO = serviceBlockPacket.readBits(2); 696 int fillR = serviceBlockPacket.readBits(2); 697 int fillG = serviceBlockPacket.readBits(2); 698 int fillB = serviceBlockPacket.readBits(2); 699 int fillColor = CueInfoBuilder.getArgbColorFromCeaColor(fillR, fillG, fillB, fillO); 700 // second byte 701 int borderType = serviceBlockPacket.readBits(2); // only the lower 2 bits of borderType 702 int borderR = serviceBlockPacket.readBits(2); 703 int borderG = serviceBlockPacket.readBits(2); 704 int borderB = serviceBlockPacket.readBits(2); 705 int borderColor = CueInfoBuilder.getArgbColorFromCeaColor(borderR, borderG, borderB); 706 // third byte 707 if (serviceBlockPacket.readBit()) { 708 borderType |= 0x04; // set the top bit of the 3-bit borderType 709 } 710 boolean wordWrapToggle = serviceBlockPacket.readBit(); 711 int printDirection = serviceBlockPacket.readBits(2); 712 int scrollDirection = serviceBlockPacket.readBits(2); 713 int justification = serviceBlockPacket.readBits(2); 714 // fourth byte 715 // Note that we don't intend to support display effects 716 serviceBlockPacket.skipBits(8); // effectSpeed(4), effectDirection(2), displayEffect(2) 717 718 currentCueInfoBuilder.setWindowAttributes( 719 fillColor, 720 borderColor, 721 wordWrapToggle, 722 borderType, 723 printDirection, 724 scrollDirection, 725 justification); 726 } 727 handleDefineWindow(int window)728 private void handleDefineWindow(int window) { 729 CueInfoBuilder cueInfoBuilder = cueInfoBuilders[window]; 730 731 // the DefineWindow command contains 6 bytes of data 732 // first byte 733 serviceBlockPacket.skipBits(2); // null padding 734 boolean visible = serviceBlockPacket.readBit(); 735 boolean rowLock = serviceBlockPacket.readBit(); 736 boolean columnLock = serviceBlockPacket.readBit(); 737 int priority = serviceBlockPacket.readBits(3); 738 // second byte 739 boolean relativePositioning = serviceBlockPacket.readBit(); 740 int verticalAnchor = serviceBlockPacket.readBits(7); 741 // third byte 742 int horizontalAnchor = serviceBlockPacket.readBits(8); 743 // fourth byte 744 int anchorId = serviceBlockPacket.readBits(4); 745 int rowCount = serviceBlockPacket.readBits(4); 746 // fifth byte 747 serviceBlockPacket.skipBits(2); // null padding 748 int columnCount = serviceBlockPacket.readBits(6); 749 // sixth byte 750 serviceBlockPacket.skipBits(2); // null padding 751 int windowStyle = serviceBlockPacket.readBits(3); 752 int penStyle = serviceBlockPacket.readBits(3); 753 754 cueInfoBuilder.defineWindow( 755 visible, 756 rowLock, 757 columnLock, 758 priority, 759 relativePositioning, 760 verticalAnchor, 761 horizontalAnchor, 762 rowCount, 763 columnCount, 764 anchorId, 765 windowStyle, 766 penStyle); 767 } 768 getDisplayCues()769 private List<Cue> getDisplayCues() { 770 List<Cea708CueInfo> displayCueInfos = new ArrayList<>(); 771 for (int i = 0; i < NUM_WINDOWS; i++) { 772 if (!cueInfoBuilders[i].isEmpty() && cueInfoBuilders[i].isVisible()) { 773 @Nullable Cea708CueInfo cueInfo = cueInfoBuilders[i].build(); 774 if (cueInfo != null) { 775 displayCueInfos.add(cueInfo); 776 } 777 } 778 } 779 Collections.sort( 780 displayCueInfos, 781 (thisInfo, thatInfo) -> Integer.compare(thisInfo.priority, thatInfo.priority)); 782 List<Cue> displayCues = new ArrayList<>(displayCueInfos.size()); 783 for (int i = 0; i < displayCueInfos.size(); i++) { 784 displayCues.add(displayCueInfos.get(i).cue); 785 } 786 return Collections.unmodifiableList(displayCues); 787 } 788 resetCueBuilders()789 private void resetCueBuilders() { 790 for (int i = 0; i < NUM_WINDOWS; i++) { 791 cueInfoBuilders[i].reset(); 792 } 793 } 794 795 private static final class DtvCcPacket { 796 797 public final int sequenceNumber; 798 public final int packetSize; 799 public final byte[] packetData; 800 801 int currentIndex; 802 DtvCcPacket(int sequenceNumber, int packetSize)803 public DtvCcPacket(int sequenceNumber, int packetSize) { 804 this.sequenceNumber = sequenceNumber; 805 this.packetSize = packetSize; 806 packetData = new byte[2 * packetSize - 1]; 807 currentIndex = 0; 808 } 809 810 } 811 812 // TODO: There is a lot of overlap between Cea708Decoder.CueInfoBuilder and 813 // Cea608Decoder.CueBuilder which could be refactored into a separate class. 814 private static final class CueInfoBuilder { 815 816 private static final int RELATIVE_CUE_SIZE = 99; 817 private static final int VERTICAL_SIZE = 74; 818 private static final int HORIZONTAL_SIZE = 209; 819 820 private static final int DEFAULT_PRIORITY = 4; 821 822 private static final int MAXIMUM_ROW_COUNT = 15; 823 824 private static final int JUSTIFICATION_LEFT = 0; 825 private static final int JUSTIFICATION_RIGHT = 1; 826 private static final int JUSTIFICATION_CENTER = 2; 827 private static final int JUSTIFICATION_FULL = 3; 828 829 private static final int DIRECTION_LEFT_TO_RIGHT = 0; 830 private static final int DIRECTION_RIGHT_TO_LEFT = 1; 831 private static final int DIRECTION_TOP_TO_BOTTOM = 2; 832 private static final int DIRECTION_BOTTOM_TO_TOP = 3; 833 834 // TODO: Add other border/edge types when utilized. 835 private static final int BORDER_AND_EDGE_TYPE_NONE = 0; 836 private static final int BORDER_AND_EDGE_TYPE_UNIFORM = 3; 837 838 public static final int COLOR_SOLID_WHITE = getArgbColorFromCeaColor(2, 2, 2, 0); 839 public static final int COLOR_SOLID_BLACK = getArgbColorFromCeaColor(0, 0, 0, 0); 840 public static final int COLOR_TRANSPARENT = getArgbColorFromCeaColor(0, 0, 0, 3); 841 842 // TODO: Add other sizes when utilized. 843 private static final int PEN_SIZE_STANDARD = 1; 844 845 // TODO: Add other pen font styles when utilized. 846 private static final int PEN_FONT_STYLE_DEFAULT = 0; 847 private static final int PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS = 1; 848 private static final int PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS = 2; 849 private static final int PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS = 3; 850 private static final int PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS = 4; 851 852 // TODO: Add other pen offsets when utilized. 853 private static final int PEN_OFFSET_NORMAL = 1; 854 855 // The window style properties are specified in the CEA-708 specification. 856 private static final int[] WINDOW_STYLE_JUSTIFICATION = new int[] { 857 JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, 858 JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_CENTER, 859 JUSTIFICATION_LEFT 860 }; 861 private static final int[] WINDOW_STYLE_PRINT_DIRECTION = new int[] { 862 DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, 863 DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, 864 DIRECTION_TOP_TO_BOTTOM 865 }; 866 private static final int[] WINDOW_STYLE_SCROLL_DIRECTION = new int[] { 867 DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, 868 DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, 869 DIRECTION_RIGHT_TO_LEFT 870 }; 871 private static final boolean[] WINDOW_STYLE_WORD_WRAP = new boolean[] { 872 false, false, false, true, true, true, false 873 }; 874 private static final int[] WINDOW_STYLE_FILL = new int[] { 875 COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, 876 COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK 877 }; 878 879 // The pen style properties are specified in the CEA-708 specification. 880 private static final int[] PEN_STYLE_FONT_STYLE = new int[] { 881 PEN_FONT_STYLE_DEFAULT, PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS, 882 PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS, PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS, 883 PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS, 884 PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS, 885 PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS 886 }; 887 private static final int[] PEN_STYLE_EDGE_TYPE = new int[] { 888 BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, 889 BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_UNIFORM, 890 BORDER_AND_EDGE_TYPE_UNIFORM 891 }; 892 private static final int[] PEN_STYLE_BACKGROUND = new int[] { 893 COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, 894 COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_TRANSPARENT}; 895 896 private final List<SpannableString> rolledUpCaptions; 897 private final SpannableStringBuilder captionStringBuilder; 898 899 // Window/Cue properties 900 private boolean defined; 901 private boolean visible; 902 private int priority; 903 private boolean relativePositioning; 904 private int verticalAnchor; 905 private int horizontalAnchor; 906 private int anchorId; 907 private int rowCount; 908 private boolean rowLock; 909 private int justification; 910 private int windowStyleId; 911 private int penStyleId; 912 private int windowFillColor; 913 914 // Pen/Text properties 915 private int italicsStartPosition; 916 private int underlineStartPosition; 917 private int foregroundColorStartPosition; 918 private int foregroundColor; 919 private int backgroundColorStartPosition; 920 private int backgroundColor; 921 private int row; 922 CueInfoBuilder()923 public CueInfoBuilder() { 924 rolledUpCaptions = new ArrayList<>(); 925 captionStringBuilder = new SpannableStringBuilder(); 926 reset(); 927 } 928 isEmpty()929 public boolean isEmpty() { 930 return !isDefined() || (rolledUpCaptions.isEmpty() && captionStringBuilder.length() == 0); 931 } 932 reset()933 public void reset() { 934 clear(); 935 936 defined = false; 937 visible = false; 938 priority = DEFAULT_PRIORITY; 939 relativePositioning = false; 940 verticalAnchor = 0; 941 horizontalAnchor = 0; 942 anchorId = 0; 943 rowCount = MAXIMUM_ROW_COUNT; 944 rowLock = true; 945 justification = JUSTIFICATION_LEFT; 946 windowStyleId = 0; 947 penStyleId = 0; 948 windowFillColor = COLOR_SOLID_BLACK; 949 950 foregroundColor = COLOR_SOLID_WHITE; 951 backgroundColor = COLOR_SOLID_BLACK; 952 } 953 clear()954 public void clear() { 955 rolledUpCaptions.clear(); 956 captionStringBuilder.clear(); 957 italicsStartPosition = C.POSITION_UNSET; 958 underlineStartPosition = C.POSITION_UNSET; 959 foregroundColorStartPosition = C.POSITION_UNSET; 960 backgroundColorStartPosition = C.POSITION_UNSET; 961 row = 0; 962 } 963 isDefined()964 public boolean isDefined() { 965 return defined; 966 } 967 setVisibility(boolean visible)968 public void setVisibility(boolean visible) { 969 this.visible = visible; 970 } 971 isVisible()972 public boolean isVisible() { 973 return visible; 974 } 975 defineWindow(boolean visible, boolean rowLock, boolean columnLock, int priority, boolean relativePositioning, int verticalAnchor, int horizontalAnchor, int rowCount, int columnCount, int anchorId, int windowStyleId, int penStyleId)976 public void defineWindow(boolean visible, boolean rowLock, boolean columnLock, int priority, 977 boolean relativePositioning, int verticalAnchor, int horizontalAnchor, int rowCount, 978 int columnCount, int anchorId, int windowStyleId, int penStyleId) { 979 this.defined = true; 980 this.visible = visible; 981 this.rowLock = rowLock; 982 this.priority = priority; 983 this.relativePositioning = relativePositioning; 984 this.verticalAnchor = verticalAnchor; 985 this.horizontalAnchor = horizontalAnchor; 986 this.anchorId = anchorId; 987 988 // Decoders must add one to rowCount to get the desired number of rows. 989 if (this.rowCount != rowCount + 1) { 990 this.rowCount = rowCount + 1; 991 992 // Trim any rolled up captions that are no longer valid, if applicable. 993 while ((rowLock && (rolledUpCaptions.size() >= this.rowCount)) 994 || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) { 995 rolledUpCaptions.remove(0); 996 } 997 } 998 999 // TODO: Add support for column lock and count. 1000 1001 if (windowStyleId != 0 && this.windowStyleId != windowStyleId) { 1002 this.windowStyleId = windowStyleId; 1003 // windowStyleId is 1-based. 1004 int windowStyleIdIndex = windowStyleId - 1; 1005 // Note that Border type and border color are the same for all window styles. 1006 setWindowAttributes(WINDOW_STYLE_FILL[windowStyleIdIndex], COLOR_TRANSPARENT, 1007 WINDOW_STYLE_WORD_WRAP[windowStyleIdIndex], BORDER_AND_EDGE_TYPE_NONE, 1008 WINDOW_STYLE_PRINT_DIRECTION[windowStyleIdIndex], 1009 WINDOW_STYLE_SCROLL_DIRECTION[windowStyleIdIndex], 1010 WINDOW_STYLE_JUSTIFICATION[windowStyleIdIndex]); 1011 } 1012 1013 if (penStyleId != 0 && this.penStyleId != penStyleId) { 1014 this.penStyleId = penStyleId; 1015 // penStyleId is 1-based. 1016 int penStyleIdIndex = penStyleId - 1; 1017 // Note that pen size, offset, italics, underline, foreground color, and foreground 1018 // opacity are the same for all pen styles. 1019 setPenAttributes(0, PEN_OFFSET_NORMAL, PEN_SIZE_STANDARD, false, false, 1020 PEN_STYLE_EDGE_TYPE[penStyleIdIndex], PEN_STYLE_FONT_STYLE[penStyleIdIndex]); 1021 setPenColor(COLOR_SOLID_WHITE, PEN_STYLE_BACKGROUND[penStyleIdIndex], COLOR_SOLID_BLACK); 1022 } 1023 } 1024 1025 setWindowAttributes(int fillColor, int borderColor, boolean wordWrapToggle, int borderType, int printDirection, int scrollDirection, int justification)1026 public void setWindowAttributes(int fillColor, int borderColor, boolean wordWrapToggle, 1027 int borderType, int printDirection, int scrollDirection, int justification) { 1028 this.windowFillColor = fillColor; 1029 // TODO: Add support for border color and types. 1030 // TODO: Add support for word wrap. 1031 // TODO: Add support for other scroll directions. 1032 // TODO: Add support for other print directions. 1033 this.justification = justification; 1034 1035 } 1036 setPenAttributes(int textTag, int offset, int penSize, boolean italicsToggle, boolean underlineToggle, int edgeType, int fontStyle)1037 public void setPenAttributes(int textTag, int offset, int penSize, boolean italicsToggle, 1038 boolean underlineToggle, int edgeType, int fontStyle) { 1039 // TODO: Add support for text tags. 1040 // TODO: Add support for other offsets. 1041 // TODO: Add support for other pen sizes. 1042 1043 if (italicsStartPosition != C.POSITION_UNSET) { 1044 if (!italicsToggle) { 1045 captionStringBuilder.setSpan(new StyleSpan(Typeface.ITALIC), italicsStartPosition, 1046 captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1047 italicsStartPosition = C.POSITION_UNSET; 1048 } 1049 } else if (italicsToggle) { 1050 italicsStartPosition = captionStringBuilder.length(); 1051 } 1052 1053 if (underlineStartPosition != C.POSITION_UNSET) { 1054 if (!underlineToggle) { 1055 captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, 1056 captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1057 underlineStartPosition = C.POSITION_UNSET; 1058 } 1059 } else if (underlineToggle) { 1060 underlineStartPosition = captionStringBuilder.length(); 1061 } 1062 1063 // TODO: Add support for edge types. 1064 // TODO: Add support for other font styles. 1065 } 1066 setPenColor(int foregroundColor, int backgroundColor, int edgeColor)1067 public void setPenColor(int foregroundColor, int backgroundColor, int edgeColor) { 1068 if (foregroundColorStartPosition != C.POSITION_UNSET) { 1069 if (this.foregroundColor != foregroundColor) { 1070 captionStringBuilder.setSpan(new ForegroundColorSpan(this.foregroundColor), 1071 foregroundColorStartPosition, captionStringBuilder.length(), 1072 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1073 } 1074 } 1075 if (foregroundColor != COLOR_SOLID_WHITE) { 1076 foregroundColorStartPosition = captionStringBuilder.length(); 1077 this.foregroundColor = foregroundColor; 1078 } 1079 1080 if (backgroundColorStartPosition != C.POSITION_UNSET) { 1081 if (this.backgroundColor != backgroundColor) { 1082 captionStringBuilder.setSpan(new BackgroundColorSpan(this.backgroundColor), 1083 backgroundColorStartPosition, captionStringBuilder.length(), 1084 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1085 } 1086 } 1087 if (backgroundColor != COLOR_SOLID_BLACK) { 1088 backgroundColorStartPosition = captionStringBuilder.length(); 1089 this.backgroundColor = backgroundColor; 1090 } 1091 1092 // TODO: Add support for edge color. 1093 } 1094 setPenLocation(int row, int column)1095 public void setPenLocation(int row, int column) { 1096 // TODO: Support moving the pen location with a window properly. 1097 1098 // Until we support proper pen locations, if we encounter a row that's different from the 1099 // previous one, we should append a new line. Otherwise, we'll see strings that should be 1100 // on new lines concatenated with the previous, resulting in 2 words being combined, as 1101 // well as potentially drawing beyond the width of the window/screen. 1102 if (this.row != row) { 1103 append('\n'); 1104 } 1105 this.row = row; 1106 } 1107 backspace()1108 public void backspace() { 1109 int length = captionStringBuilder.length(); 1110 if (length > 0) { 1111 captionStringBuilder.delete(length - 1, length); 1112 } 1113 } 1114 append(char text)1115 public void append(char text) { 1116 if (text == '\n') { 1117 rolledUpCaptions.add(buildSpannableString()); 1118 captionStringBuilder.clear(); 1119 1120 if (italicsStartPosition != C.POSITION_UNSET) { 1121 italicsStartPosition = 0; 1122 } 1123 if (underlineStartPosition != C.POSITION_UNSET) { 1124 underlineStartPosition = 0; 1125 } 1126 if (foregroundColorStartPosition != C.POSITION_UNSET) { 1127 foregroundColorStartPosition = 0; 1128 } 1129 if (backgroundColorStartPosition != C.POSITION_UNSET) { 1130 backgroundColorStartPosition = 0; 1131 } 1132 1133 while ((rowLock && (rolledUpCaptions.size() >= rowCount)) 1134 || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) { 1135 rolledUpCaptions.remove(0); 1136 } 1137 } else { 1138 captionStringBuilder.append(text); 1139 } 1140 } 1141 buildSpannableString()1142 public SpannableString buildSpannableString() { 1143 SpannableStringBuilder spannableStringBuilder = 1144 new SpannableStringBuilder(captionStringBuilder); 1145 int length = spannableStringBuilder.length(); 1146 1147 if (length > 0) { 1148 if (italicsStartPosition != C.POSITION_UNSET) { 1149 spannableStringBuilder.setSpan(new StyleSpan(Typeface.ITALIC), italicsStartPosition, 1150 length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1151 } 1152 1153 if (underlineStartPosition != C.POSITION_UNSET) { 1154 spannableStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, 1155 length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1156 } 1157 1158 if (foregroundColorStartPosition != C.POSITION_UNSET) { 1159 spannableStringBuilder.setSpan(new ForegroundColorSpan(foregroundColor), 1160 foregroundColorStartPosition, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1161 } 1162 1163 if (backgroundColorStartPosition != C.POSITION_UNSET) { 1164 spannableStringBuilder.setSpan(new BackgroundColorSpan(backgroundColor), 1165 backgroundColorStartPosition, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 1166 } 1167 } 1168 1169 return new SpannableString(spannableStringBuilder); 1170 } 1171 1172 @Nullable build()1173 public Cea708CueInfo build() { 1174 if (isEmpty()) { 1175 // The cue is empty. 1176 return null; 1177 } 1178 1179 SpannableStringBuilder cueString = new SpannableStringBuilder(); 1180 1181 // Add any rolled up captions, separated by new lines. 1182 for (int i = 0; i < rolledUpCaptions.size(); i++) { 1183 cueString.append(rolledUpCaptions.get(i)); 1184 cueString.append('\n'); 1185 } 1186 // Add the current line. 1187 cueString.append(buildSpannableString()); 1188 1189 // TODO: Add support for right-to-left languages (i.e. where right would correspond to normal 1190 // alignment). 1191 Alignment alignment; 1192 switch (justification) { 1193 case JUSTIFICATION_FULL: 1194 // TODO: Add support for full justification. 1195 case JUSTIFICATION_LEFT: 1196 alignment = Alignment.ALIGN_NORMAL; 1197 break; 1198 case JUSTIFICATION_RIGHT: 1199 alignment = Alignment.ALIGN_OPPOSITE; 1200 break; 1201 case JUSTIFICATION_CENTER: 1202 alignment = Alignment.ALIGN_CENTER; 1203 break; 1204 default: 1205 throw new IllegalArgumentException("Unexpected justification value: " + justification); 1206 } 1207 1208 float position; 1209 float line; 1210 if (relativePositioning) { 1211 position = (float) horizontalAnchor / RELATIVE_CUE_SIZE; 1212 line = (float) verticalAnchor / RELATIVE_CUE_SIZE; 1213 } else { 1214 position = (float) horizontalAnchor / HORIZONTAL_SIZE; 1215 line = (float) verticalAnchor / VERTICAL_SIZE; 1216 } 1217 // Apply screen-edge padding to the line and position. 1218 position = (position * 0.9f) + 0.05f; 1219 line = (line * 0.9f) + 0.05f; 1220 1221 // anchorId specifies where the anchor should be placed on the caption cue/window. The 9 1222 // possible configurations are as follows: 1223 // 0-----1-----2 1224 // | | 1225 // 3 4 5 1226 // | | 1227 // 6-----7-----8 1228 @AnchorType int verticalAnchorType; 1229 if (anchorId % 3 == 0) { 1230 verticalAnchorType = Cue.ANCHOR_TYPE_START; 1231 } else if (anchorId % 3 == 1) { 1232 verticalAnchorType = Cue.ANCHOR_TYPE_MIDDLE; 1233 } else { 1234 verticalAnchorType = Cue.ANCHOR_TYPE_END; 1235 } 1236 // TODO: Add support for right-to-left languages (i.e. where start is on the right). 1237 @AnchorType int horizontalAnchorType; 1238 if (anchorId / 3 == 0) { 1239 horizontalAnchorType = Cue.ANCHOR_TYPE_START; 1240 } else if (anchorId / 3 == 1) { 1241 horizontalAnchorType = Cue.ANCHOR_TYPE_MIDDLE; 1242 } else { 1243 horizontalAnchorType = Cue.ANCHOR_TYPE_END; 1244 } 1245 1246 boolean windowColorSet = (windowFillColor != COLOR_SOLID_BLACK); 1247 1248 return new Cea708CueInfo( 1249 cueString, 1250 alignment, 1251 line, 1252 Cue.LINE_TYPE_FRACTION, 1253 verticalAnchorType, 1254 position, 1255 horizontalAnchorType, 1256 Cue.DIMEN_UNSET, 1257 windowColorSet, 1258 windowFillColor, 1259 priority); 1260 } 1261 getArgbColorFromCeaColor(int red, int green, int blue)1262 public static int getArgbColorFromCeaColor(int red, int green, int blue) { 1263 return getArgbColorFromCeaColor(red, green, blue, 0); 1264 } 1265 getArgbColorFromCeaColor(int red, int green, int blue, int opacity)1266 public static int getArgbColorFromCeaColor(int red, int green, int blue, int opacity) { 1267 Assertions.checkIndex(red, 0, 4); 1268 Assertions.checkIndex(green, 0, 4); 1269 Assertions.checkIndex(blue, 0, 4); 1270 Assertions.checkIndex(opacity, 0, 4); 1271 1272 int alpha; 1273 switch (opacity) { 1274 case 0: 1275 case 1: 1276 // Note the value of '1' is actually FLASH, but we don't support that. 1277 alpha = 255; 1278 break; 1279 case 2: 1280 alpha = 127; 1281 break; 1282 case 3: 1283 alpha = 0; 1284 break; 1285 default: 1286 alpha = 255; 1287 } 1288 1289 // TODO: Add support for the Alternative Minimum Color List or the full 64 RGB combinations. 1290 1291 // Return values based on the Minimum Color List 1292 return Color.argb(alpha, 1293 (red > 1 ? 255 : 0), 1294 (green > 1 ? 255 : 0), 1295 (blue > 1 ? 255 : 0)); 1296 } 1297 } 1298 1299 /** A {@link Cue} for CEA-708. */ 1300 private static final class Cea708CueInfo { 1301 1302 public final Cue cue; 1303 1304 /** The priority of the cue box. */ 1305 public final int priority; 1306 1307 /** 1308 * @param text See {@link Cue#text}. 1309 * @param textAlignment See {@link Cue#textAlignment}. 1310 * @param line See {@link Cue#line}. 1311 * @param lineType See {@link Cue#lineType}. 1312 * @param lineAnchor See {@link Cue#lineAnchor}. 1313 * @param position See {@link Cue#position}. 1314 * @param positionAnchor See {@link Cue#positionAnchor}. 1315 * @param size See {@link Cue#size}. 1316 * @param windowColorSet See {@link Cue#windowColorSet}. 1317 * @param windowColor See {@link Cue#windowColor}. 1318 * @param priority See (@link #priority}. 1319 */ Cea708CueInfo( CharSequence text, Alignment textAlignment, float line, @Cue.LineType int lineType, @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size, boolean windowColorSet, int windowColor, int priority)1320 public Cea708CueInfo( 1321 CharSequence text, 1322 Alignment textAlignment, 1323 float line, 1324 @Cue.LineType int lineType, 1325 @AnchorType int lineAnchor, 1326 float position, 1327 @AnchorType int positionAnchor, 1328 float size, 1329 boolean windowColorSet, 1330 int windowColor, 1331 int priority) { 1332 this.cue = 1333 new Cue( 1334 text, 1335 textAlignment, 1336 line, 1337 lineType, 1338 lineAnchor, 1339 position, 1340 positionAnchor, 1341 size, 1342 windowColorSet, 1343 windowColor); 1344 this.priority = priority; 1345 } 1346 } 1347 } 1348