ClosedCaptionRenderer.java revision bdfd91024767893829017918ab565f224ccd35ec
1/* 2 * Copyright (C) 2014 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 17package android.media; 18 19import android.content.Context; 20import android.graphics.Color; 21import android.graphics.Rect; 22import android.graphics.Typeface; 23import android.util.AttributeSet; 24import android.util.Log; 25import android.util.TypedValue; 26import android.view.Gravity; 27import android.view.View; 28import android.view.ViewGroup; 29import android.view.accessibility.CaptioningManager; 30import android.view.accessibility.CaptioningManager.CaptionStyle; 31import android.view.accessibility.CaptioningManager.CaptioningChangeListener; 32import android.widget.LinearLayout; 33import android.widget.TextView; 34 35import java.util.ArrayList; 36import java.util.Arrays; 37import java.util.Vector; 38 39import android.text.SpannableStringBuilder; 40import android.text.Spannable; 41import android.text.style.BackgroundColorSpan; 42import android.text.style.StyleSpan; 43 44/** @hide */ 45public class ClosedCaptionRenderer extends SubtitleController.Renderer { 46 private final Context mContext; 47 private ClosedCaptionWidget mRenderingWidget; 48 49 public ClosedCaptionRenderer(Context context) { 50 mContext = context; 51 } 52 53 @Override 54 public boolean supports(MediaFormat format) { 55 if (format.containsKey(MediaFormat.KEY_MIME)) { 56 return format.getString(MediaFormat.KEY_MIME).equals( 57 MediaPlayer.MEDIA_MIMETYPE_TEXT_CEA_608); 58 } 59 return false; 60 } 61 62 @Override 63 public SubtitleTrack createTrack(MediaFormat format) { 64 if (mRenderingWidget == null) { 65 mRenderingWidget = new ClosedCaptionWidget(mContext); 66 } 67 return new ClosedCaptionTrack(mRenderingWidget, format); 68 } 69} 70 71/** @hide */ 72class ClosedCaptionTrack extends SubtitleTrack { 73 private final ClosedCaptionWidget mRenderingWidget; 74 private final CCParser mCCParser; 75 76 ClosedCaptionTrack(ClosedCaptionWidget renderingWidget, MediaFormat format) { 77 super(format); 78 79 mRenderingWidget = renderingWidget; 80 mCCParser = new CCParser(renderingWidget); 81 } 82 83 @Override 84 public void onData(byte[] data, boolean eos, long runID) { 85 mCCParser.parse(data); 86 } 87 88 @Override 89 public RenderingWidget getRenderingWidget() { 90 return mRenderingWidget; 91 } 92 93 @Override 94 public void updateView(Vector<Cue> activeCues) { 95 // Overriding with NO-OP, CC rendering by-passes this 96 } 97} 98 99/** 100 * @hide 101 * 102 * CCParser processes CEA-608 closed caption data. 103 * 104 * It calls back into OnDisplayChangedListener upon 105 * display change with styled text for rendering. 106 * 107 */ 108class CCParser { 109 public static final int MAX_ROWS = 15; 110 public static final int MAX_COLS = 32; 111 112 private static final String TAG = "CCParser"; 113 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 114 115 private static final int INVALID = -1; 116 117 // EIA-CEA-608: Table 70 - Control Codes 118 private static final int RCL = 0x20; 119 private static final int BS = 0x21; 120 private static final int AOF = 0x22; 121 private static final int AON = 0x23; 122 private static final int DER = 0x24; 123 private static final int RU2 = 0x25; 124 private static final int RU3 = 0x26; 125 private static final int RU4 = 0x27; 126 private static final int FON = 0x28; 127 private static final int RDC = 0x29; 128 private static final int TR = 0x2a; 129 private static final int RTD = 0x2b; 130 private static final int EDM = 0x2c; 131 private static final int CR = 0x2d; 132 private static final int ENM = 0x2e; 133 private static final int EOC = 0x2f; 134 135 // Transparent Space 136 private static final char TS = '\u00A0'; 137 138 // Captioning Modes 139 private static final int MODE_UNKNOWN = 0; 140 private static final int MODE_PAINT_ON = 1; 141 private static final int MODE_ROLL_UP = 2; 142 private static final int MODE_POP_ON = 3; 143 private static final int MODE_TEXT = 4; 144 145 private final DisplayListener mListener; 146 147 private int mMode = MODE_PAINT_ON; 148 private int mRollUpSize = 4; 149 150 private CCMemory mDisplay = new CCMemory(); 151 private CCMemory mNonDisplay = new CCMemory(); 152 private CCMemory mTextMem = new CCMemory(); 153 154 CCParser(DisplayListener listener) { 155 mListener = listener; 156 } 157 158 void parse(byte[] data) { 159 CCData[] ccData = CCData.fromByteArray(data); 160 161 for (int i = 0; i < ccData.length; i++) { 162 if (DEBUG) { 163 Log.d(TAG, ccData[i].toString()); 164 } 165 166 if (handleCtrlCode(ccData[i]) 167 || handleTabOffsets(ccData[i]) 168 || handlePACCode(ccData[i]) 169 || handleMidRowCode(ccData[i])) { 170 continue; 171 } 172 173 handleDisplayableChars(ccData[i]); 174 } 175 } 176 177 interface DisplayListener { 178 public void onDisplayChanged(SpannableStringBuilder[] styledTexts); 179 public CaptionStyle getCaptionStyle(); 180 } 181 182 private CCMemory getMemory() { 183 // get the CC memory to operate on for current mode 184 switch (mMode) { 185 case MODE_POP_ON: 186 return mNonDisplay; 187 case MODE_TEXT: 188 // TODO(chz): support only caption mode for now, 189 // in text mode, dump everything to text mem. 190 return mTextMem; 191 case MODE_PAINT_ON: 192 case MODE_ROLL_UP: 193 return mDisplay; 194 default: 195 Log.w(TAG, "unrecoginized mode: " + mMode); 196 } 197 return mDisplay; 198 } 199 200 private boolean handleDisplayableChars(CCData ccData) { 201 if (!ccData.isDisplayableChar()) { 202 return false; 203 } 204 205 // Extended char includes 1 automatic backspace 206 if (ccData.isExtendedChar()) { 207 getMemory().bs(); 208 } 209 210 getMemory().writeText(ccData.getDisplayText()); 211 212 if (mMode == MODE_PAINT_ON || mMode == MODE_ROLL_UP) { 213 updateDisplay(); 214 } 215 216 return true; 217 } 218 219 private boolean handleMidRowCode(CCData ccData) { 220 StyleCode m = ccData.getMidRow(); 221 if (m != null) { 222 getMemory().writeMidRowCode(m); 223 return true; 224 } 225 return false; 226 } 227 228 private boolean handlePACCode(CCData ccData) { 229 PAC pac = ccData.getPAC(); 230 231 if (pac != null) { 232 if (mMode == MODE_ROLL_UP) { 233 getMemory().moveBaselineTo(pac.getRow(), mRollUpSize); 234 } 235 getMemory().writePAC(pac); 236 return true; 237 } 238 239 return false; 240 } 241 242 private boolean handleTabOffsets(CCData ccData) { 243 int tabs = ccData.getTabOffset(); 244 245 if (tabs > 0) { 246 getMemory().tab(tabs); 247 return true; 248 } 249 250 return false; 251 } 252 253 private boolean handleCtrlCode(CCData ccData) { 254 int ctrlCode = ccData.getCtrlCode(); 255 switch(ctrlCode) { 256 case RCL: 257 // select pop-on style 258 mMode = MODE_POP_ON; 259 break; 260 case BS: 261 getMemory().bs(); 262 break; 263 case DER: 264 getMemory().der(); 265 break; 266 case RU2: 267 case RU3: 268 case RU4: 269 mRollUpSize = (ctrlCode - 0x23); 270 // erase memory if currently in other style 271 if (mMode != MODE_ROLL_UP) { 272 mDisplay.erase(); 273 mNonDisplay.erase(); 274 } 275 // select roll-up style 276 mMode = MODE_ROLL_UP; 277 break; 278 case FON: 279 Log.i(TAG, "Flash On"); 280 break; 281 case RDC: 282 // select paint-on style 283 mMode = MODE_PAINT_ON; 284 break; 285 case TR: 286 mMode = MODE_TEXT; 287 mTextMem.erase(); 288 break; 289 case RTD: 290 mMode = MODE_TEXT; 291 break; 292 case EDM: 293 // erase display memory 294 mDisplay.erase(); 295 updateDisplay(); 296 break; 297 case CR: 298 if (mMode == MODE_ROLL_UP) { 299 getMemory().rollUp(mRollUpSize); 300 } else { 301 getMemory().cr(); 302 } 303 if (mMode == MODE_ROLL_UP) { 304 updateDisplay(); 305 } 306 break; 307 case ENM: 308 // erase non-display memory 309 mNonDisplay.erase(); 310 break; 311 case EOC: 312 // swap display/non-display memory 313 swapMemory(); 314 // switch to pop-on style 315 mMode = MODE_POP_ON; 316 updateDisplay(); 317 break; 318 case INVALID: 319 default: 320 // not handled 321 return false; 322 } 323 324 // handled 325 return true; 326 } 327 328 private void updateDisplay() { 329 if (mListener != null) { 330 CaptionStyle captionStyle = mListener.getCaptionStyle(); 331 mListener.onDisplayChanged(mDisplay.getStyledText(captionStyle)); 332 } 333 } 334 335 private void swapMemory() { 336 CCMemory temp = mDisplay; 337 mDisplay = mNonDisplay; 338 mNonDisplay = temp; 339 } 340 341 private static class StyleCode { 342 static final int COLOR_WHITE = 0; 343 static final int COLOR_GREEN = 1; 344 static final int COLOR_BLUE = 2; 345 static final int COLOR_CYAN = 3; 346 static final int COLOR_RED = 4; 347 static final int COLOR_YELLOW = 5; 348 static final int COLOR_MAGENTA = 6; 349 static final int COLOR_INVALID = 7; 350 351 static final int STYLE_ITALICS = 0x00000001; 352 static final int STYLE_UNDERLINE = 0x00000002; 353 354 static final String[] mColorMap = { 355 "WHITE", "GREEN", "BLUE", "CYAN", "RED", "YELLOW", "MAGENTA", "INVALID" 356 }; 357 358 final int mStyle; 359 final int mColor; 360 361 static StyleCode fromByte(byte data2) { 362 int style = 0; 363 int color = (data2 >> 1) & 0x7; 364 365 if ((data2 & 0x1) != 0) { 366 style |= STYLE_UNDERLINE; 367 } 368 369 if (color == COLOR_INVALID) { 370 // WHITE ITALICS 371 color = COLOR_WHITE; 372 style |= STYLE_ITALICS; 373 } 374 375 return new StyleCode(style, color); 376 } 377 378 StyleCode(int style, int color) { 379 mStyle = style; 380 mColor = color; 381 } 382 383 boolean isItalics() { 384 return (mStyle & STYLE_ITALICS) != 0; 385 } 386 387 int getColor() { 388 return mColor; 389 } 390 391 @Override 392 public String toString() { 393 StringBuilder str = new StringBuilder(); 394 str.append("{"); 395 str.append(mColorMap[mColor]); 396 if ((mStyle & STYLE_ITALICS) != 0) { 397 str.append(", ITALICS"); 398 } 399 if ((mStyle & STYLE_UNDERLINE) != 0) { 400 str.append(", UNDERLINE"); 401 } 402 str.append("}"); 403 404 return str.toString(); 405 } 406 } 407 408 private static class PAC extends StyleCode { 409 final int mRow; 410 final int mCol; 411 412 static PAC fromBytes(byte data1, byte data2) { 413 int[] rowTable = {11, 1, 3, 12, 14, 5, 7, 9}; 414 int row = rowTable[data1 & 0x07] + ((data2 & 0x20) >> 5); 415 int style = 0; 416 if ((data2 & 1) != 0) { 417 style |= STYLE_UNDERLINE; 418 } 419 if ((data2 & 0x10) != 0) { 420 // indent code 421 int indent = (data2 >> 1) & 0x7; 422 return new PAC(row, indent * 4, style, COLOR_WHITE); 423 } else { 424 // style code 425 int color = (data2 >> 1) & 0x7; 426 427 if (color == COLOR_INVALID) { 428 // WHITE ITALICS 429 color = COLOR_WHITE; 430 style |= STYLE_ITALICS; 431 } 432 return new PAC(row, -1, style, color); 433 } 434 } 435 436 PAC(int row, int col, int style, int color) { 437 super(style, color); 438 mRow = row; 439 mCol = col; 440 } 441 442 boolean isIndentPAC() { 443 return (mCol >= 0); 444 } 445 446 int getRow() { 447 return mRow; 448 } 449 450 int getCol() { 451 return mCol; 452 } 453 454 @Override 455 public String toString() { 456 return String.format("{%d, %d}, %s", 457 mRow, mCol, super.toString()); 458 } 459 } 460 461 /* CCLineBuilder keeps track of displayable chars, as well as 462 * MidRow styles and PACs, for a single line of CC memory. 463 * 464 * It generates styled text via getStyledText() method. 465 */ 466 private static class CCLineBuilder { 467 private final StringBuilder mDisplayChars; 468 private final StyleCode[] mMidRowStyles; 469 private final StyleCode[] mPACStyles; 470 471 CCLineBuilder(String str) { 472 mDisplayChars = new StringBuilder(str); 473 mMidRowStyles = new StyleCode[mDisplayChars.length()]; 474 mPACStyles = new StyleCode[mDisplayChars.length()]; 475 } 476 477 void setCharAt(int index, char ch) { 478 mDisplayChars.setCharAt(index, ch); 479 mMidRowStyles[index] = null; 480 } 481 482 void setMidRowAt(int index, StyleCode m) { 483 mDisplayChars.setCharAt(index, ' '); 484 mMidRowStyles[index] = m; 485 } 486 487 void setPACAt(int index, PAC pac) { 488 mPACStyles[index] = pac; 489 } 490 491 char charAt(int index) { 492 return mDisplayChars.charAt(index); 493 } 494 495 int length() { 496 return mDisplayChars.length(); 497 } 498 499 void applyStyleSpan( 500 SpannableStringBuilder styledText, 501 StyleCode s, int start, int end) { 502 if (s.isItalics()) { 503 styledText.setSpan( 504 new StyleSpan(android.graphics.Typeface.ITALIC), 505 start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 506 } 507 } 508 509 SpannableStringBuilder getStyledText(CaptionStyle captionStyle) { 510 SpannableStringBuilder styledText = new SpannableStringBuilder(mDisplayChars); 511 int start = -1, next = 0; 512 int styleStart = -1; 513 StyleCode curStyle = null; 514 while (next < mDisplayChars.length()) { 515 StyleCode newStyle = null; 516 if (mMidRowStyles[next] != null) { 517 // apply mid-row style change 518 newStyle = mMidRowStyles[next]; 519 } else if (mPACStyles[next] != null 520 && (styleStart < 0 || start < 0)) { 521 // apply PAC style change, only if: 522 // 1. no style set, or 523 // 2. style set, but prev char is none-displayable 524 newStyle = mPACStyles[next]; 525 } 526 if (newStyle != null) { 527 curStyle = newStyle; 528 if (styleStart >= 0 && start >= 0) { 529 applyStyleSpan(styledText, newStyle, styleStart, next); 530 } 531 styleStart = next; 532 } 533 534 if (mDisplayChars.charAt(next) != TS) { 535 if (start < 0) { 536 start = next; 537 } 538 } else if (start >= 0) { 539 int expandedStart = mDisplayChars.charAt(start) == ' ' ? start : start - 1; 540 int expandedEnd = mDisplayChars.charAt(next - 1) == ' ' ? next : next + 1; 541 styledText.setSpan( 542 new BackgroundColorSpan(captionStyle.backgroundColor), 543 expandedStart, expandedEnd, 544 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 545 if (styleStart >= 0) { 546 applyStyleSpan(styledText, curStyle, styleStart, expandedEnd); 547 } 548 start = -1; 549 } 550 next++; 551 } 552 553 return styledText; 554 } 555 } 556 557 /* 558 * CCMemory models a console-style display. 559 */ 560 private static class CCMemory { 561 private final String mBlankLine; 562 private final CCLineBuilder[] mLines = new CCLineBuilder[MAX_ROWS + 2]; 563 private int mRow; 564 private int mCol; 565 566 CCMemory() { 567 char[] blank = new char[MAX_COLS + 2]; 568 Arrays.fill(blank, TS); 569 mBlankLine = new String(blank); 570 } 571 572 void erase() { 573 // erase all lines 574 for (int i = 0; i < mLines.length; i++) { 575 mLines[i] = null; 576 } 577 mRow = MAX_ROWS; 578 mCol = 1; 579 } 580 581 void der() { 582 if (mLines[mRow] != null) { 583 for (int i = 0; i < mCol; i++) { 584 if (mLines[mRow].charAt(i) != TS) { 585 for (int j = mCol; j < mLines[mRow].length(); j++) { 586 mLines[j].setCharAt(j, TS); 587 } 588 return; 589 } 590 } 591 mLines[mRow] = null; 592 } 593 } 594 595 void tab(int tabs) { 596 moveCursorByCol(tabs); 597 } 598 599 void bs() { 600 moveCursorByCol(-1); 601 if (mLines[mRow] != null) { 602 mLines[mRow].setCharAt(mCol, TS); 603 if (mCol == MAX_COLS - 1) { 604 // Spec recommendation: 605 // if cursor was at col 32, move cursor 606 // back to col 31 and erase both col 31&32 607 mLines[mRow].setCharAt(MAX_COLS, TS); 608 } 609 } 610 } 611 612 void cr() { 613 moveCursorTo(mRow + 1, 1); 614 } 615 616 void rollUp(int windowSize) { 617 int i; 618 for (i = 0; i <= mRow - windowSize; i++) { 619 mLines[i] = null; 620 } 621 int startRow = mRow - windowSize + 1; 622 if (startRow < 1) { 623 startRow = 1; 624 } 625 for (i = startRow; i < mRow; i++) { 626 mLines[i] = mLines[i + 1]; 627 } 628 for (i = mRow; i < mLines.length; i++) { 629 // clear base row 630 mLines[i] = null; 631 } 632 // default to col 1, in case PAC is not sent 633 mCol = 1; 634 } 635 636 void writeText(String text) { 637 for (int i = 0; i < text.length(); i++) { 638 getLineBuffer(mRow).setCharAt(mCol, text.charAt(i)); 639 moveCursorByCol(1); 640 } 641 } 642 643 void writeMidRowCode(StyleCode m) { 644 getLineBuffer(mRow).setMidRowAt(mCol, m); 645 moveCursorByCol(1); 646 } 647 648 void writePAC(PAC pac) { 649 if (pac.isIndentPAC()) { 650 moveCursorTo(pac.getRow(), pac.getCol()); 651 } else { 652 moveCursorToRow(pac.getRow()); 653 } 654 getLineBuffer(mRow).setPACAt(mCol, pac); 655 } 656 657 SpannableStringBuilder[] getStyledText(CaptionStyle captionStyle) { 658 ArrayList<SpannableStringBuilder> rows = 659 new ArrayList<SpannableStringBuilder>(MAX_ROWS); 660 for (int i = 1; i <= MAX_ROWS; i++) { 661 rows.add(mLines[i] != null ? 662 mLines[i].getStyledText(captionStyle) : null); 663 } 664 return rows.toArray(new SpannableStringBuilder[MAX_ROWS]); 665 } 666 667 private static int clamp(int x, int min, int max) { 668 return x < min ? min : (x > max ? max : x); 669 } 670 671 private void moveCursorTo(int row, int col) { 672 mRow = clamp(row, 1, MAX_ROWS); 673 mCol = clamp(col, 1, MAX_COLS); 674 } 675 676 private void moveCursorToRow(int row) { 677 mRow = clamp(row, 1, MAX_ROWS); 678 } 679 680 private void moveCursorByCol(int col) { 681 mCol = clamp(mCol + col, 1, MAX_COLS); 682 } 683 684 private void moveBaselineTo(int baseRow, int windowSize) { 685 if (mRow == baseRow) { 686 return; 687 } 688 int actualWindowSize = windowSize; 689 if (baseRow < actualWindowSize) { 690 actualWindowSize = baseRow; 691 } 692 if (mRow < actualWindowSize) { 693 actualWindowSize = mRow; 694 } 695 696 int i; 697 if (baseRow < mRow) { 698 // copy from bottom to top row 699 for (i = actualWindowSize - 1; i >= 0; i--) { 700 mLines[baseRow - i] = mLines[mRow - i]; 701 } 702 } else { 703 // copy from top to bottom row 704 for (i = 0; i < actualWindowSize; i++) { 705 mLines[baseRow - i] = mLines[mRow - i]; 706 } 707 } 708 // clear rest of the rows 709 for (i = 0; i <= baseRow - windowSize; i++) { 710 mLines[i] = null; 711 } 712 for (i = baseRow + 1; i < mLines.length; i++) { 713 mLines[i] = null; 714 } 715 } 716 717 private CCLineBuilder getLineBuffer(int row) { 718 if (mLines[row] == null) { 719 mLines[row] = new CCLineBuilder(mBlankLine); 720 } 721 return mLines[row]; 722 } 723 } 724 725 /* 726 * CCData parses the raw CC byte pair into displayable chars, 727 * misc control codes, Mid-Row or Preamble Address Codes. 728 */ 729 private static class CCData { 730 private final byte mType; 731 private final byte mData1; 732 private final byte mData2; 733 734 private static final String[] mCtrlCodeMap = { 735 "RCL", "BS" , "AOF", "AON", 736 "DER", "RU2", "RU3", "RU4", 737 "FON", "RDC", "TR" , "RTD", 738 "EDM", "CR" , "ENM", "EOC", 739 }; 740 741 private static final String[] mSpecialCharMap = { 742 "\u00AE", 743 "\u00B0", 744 "\u00BD", 745 "\u00BF", 746 "\u2122", 747 "\u00A2", 748 "\u00A3", 749 "\u266A", // Eighth note 750 "\u00E0", 751 "\u00A0", // Transparent space 752 "\u00E8", 753 "\u00E2", 754 "\u00EA", 755 "\u00EE", 756 "\u00F4", 757 "\u00FB", 758 }; 759 760 private static final String[] mSpanishCharMap = { 761 // Spanish and misc chars 762 "\u00C1", // A 763 "\u00C9", // E 764 "\u00D3", // I 765 "\u00DA", // O 766 "\u00DC", // U 767 "\u00FC", // u 768 "\u2018", // opening single quote 769 "\u00A1", // inverted exclamation mark 770 "*", 771 "'", 772 "\u2014", // em dash 773 "\u00A9", // Copyright 774 "\u2120", // Servicemark 775 "\u2022", // round bullet 776 "\u201C", // opening double quote 777 "\u201D", // closing double quote 778 // French 779 "\u00C0", 780 "\u00C2", 781 "\u00C7", 782 "\u00C8", 783 "\u00CA", 784 "\u00CB", 785 "\u00EB", 786 "\u00CE", 787 "\u00CF", 788 "\u00EF", 789 "\u00D4", 790 "\u00D9", 791 "\u00F9", 792 "\u00DB", 793 "\u00AB", 794 "\u00BB" 795 }; 796 797 private static final String[] mProtugueseCharMap = { 798 // Portuguese 799 "\u00C3", 800 "\u00E3", 801 "\u00CD", 802 "\u00CC", 803 "\u00EC", 804 "\u00D2", 805 "\u00F2", 806 "\u00D5", 807 "\u00F5", 808 "{", 809 "}", 810 "\\", 811 "^", 812 "_", 813 "|", 814 "~", 815 // German and misc chars 816 "\u00C4", 817 "\u00E4", 818 "\u00D6", 819 "\u00F6", 820 "\u00DF", 821 "\u00A5", 822 "\u00A4", 823 "\u2502", // vertical bar 824 "\u00C5", 825 "\u00E5", 826 "\u00D8", 827 "\u00F8", 828 "\u250C", // top-left corner 829 "\u2510", // top-right corner 830 "\u2514", // lower-left corner 831 "\u2518", // lower-right corner 832 }; 833 834 static CCData[] fromByteArray(byte[] data) { 835 CCData[] ccData = new CCData[data.length / 3]; 836 837 for (int i = 0; i < ccData.length; i++) { 838 ccData[i] = new CCData( 839 data[i * 3], 840 data[i * 3 + 1], 841 data[i * 3 + 2]); 842 } 843 844 return ccData; 845 } 846 847 CCData(byte type, byte data1, byte data2) { 848 mType = type; 849 mData1 = data1; 850 mData2 = data2; 851 } 852 853 int getCtrlCode() { 854 if ((mData1 == 0x14 || mData1 == 0x1c) 855 && mData2 >= 0x20 && mData2 <= 0x2f) { 856 return mData2; 857 } 858 return INVALID; 859 } 860 861 StyleCode getMidRow() { 862 // only support standard Mid-row codes, ignore 863 // optional background/foreground mid-row codes 864 if ((mData1 == 0x11 || mData1 == 0x19) 865 && mData2 >= 0x20 && mData2 <= 0x2f) { 866 return StyleCode.fromByte(mData2); 867 } 868 return null; 869 } 870 871 PAC getPAC() { 872 if ((mData1 & 0x70) == 0x10 873 && (mData2 & 0x40) == 0x40 874 && ((mData1 & 0x07) != 0 || (mData2 & 0x20) == 0)) { 875 return PAC.fromBytes(mData1, mData2); 876 } 877 return null; 878 } 879 880 int getTabOffset() { 881 if ((mData1 == 0x17 || mData1 == 0x1f) 882 && mData2 >= 0x21 && mData2 <= 0x23) { 883 return mData2 & 0x3; 884 } 885 return 0; 886 } 887 888 boolean isDisplayableChar() { 889 return isBasicChar() || isSpecialChar() || isExtendedChar(); 890 } 891 892 String getDisplayText() { 893 String str = getBasicChars(); 894 895 if (str == null) { 896 str = getSpecialChar(); 897 898 if (str == null) { 899 str = getExtendedChar(); 900 } 901 } 902 903 return str; 904 } 905 906 private String ctrlCodeToString(int ctrlCode) { 907 return mCtrlCodeMap[ctrlCode - 0x20]; 908 } 909 910 private boolean isBasicChar() { 911 return mData1 >= 0x20 && mData1 <= 0x7f; 912 } 913 914 private boolean isSpecialChar() { 915 return ((mData1 == 0x11 || mData1 == 0x19) 916 && mData2 >= 0x30 && mData2 <= 0x3f); 917 } 918 919 private boolean isExtendedChar() { 920 return ((mData1 == 0x12 || mData1 == 0x1A 921 || mData1 == 0x13 || mData1 == 0x1B) 922 && mData2 >= 0x20 && mData2 <= 0x3f); 923 } 924 925 private char getBasicChar(byte data) { 926 char c; 927 // replace the non-ASCII ones 928 switch (data) { 929 case 0x2A: c = '\u00E1'; break; 930 case 0x5C: c = '\u00E9'; break; 931 case 0x5E: c = '\u00ED'; break; 932 case 0x5F: c = '\u00F3'; break; 933 case 0x60: c = '\u00FA'; break; 934 case 0x7B: c = '\u00E7'; break; 935 case 0x7C: c = '\u00F7'; break; 936 case 0x7D: c = '\u00D1'; break; 937 case 0x7E: c = '\u00F1'; break; 938 case 0x7F: c = '\u2588'; break; // Full block 939 default: c = (char) data; break; 940 } 941 return c; 942 } 943 944 private String getBasicChars() { 945 if (mData1 >= 0x20 && mData1 <= 0x7f) { 946 StringBuilder builder = new StringBuilder(2); 947 builder.append(getBasicChar(mData1)); 948 if (mData2 >= 0x20 && mData2 <= 0x7f) { 949 builder.append(getBasicChar(mData2)); 950 } 951 return builder.toString(); 952 } 953 954 return null; 955 } 956 957 private String getSpecialChar() { 958 if ((mData1 == 0x11 || mData1 == 0x19) 959 && mData2 >= 0x30 && mData2 <= 0x3f) { 960 return mSpecialCharMap[mData2 - 0x30]; 961 } 962 963 return null; 964 } 965 966 private String getExtendedChar() { 967 if ((mData1 == 0x12 || mData1 == 0x1A) 968 && mData2 >= 0x20 && mData2 <= 0x3f){ 969 // 1 Spanish/French char 970 return mSpanishCharMap[mData2 - 0x20]; 971 } else if ((mData1 == 0x13 || mData1 == 0x1B) 972 && mData2 >= 0x20 && mData2 <= 0x3f){ 973 // 1 Portuguese/German/Danish char 974 return mProtugueseCharMap[mData2 - 0x20]; 975 } 976 977 return null; 978 } 979 980 @Override 981 public String toString() { 982 String str; 983 984 if (mData1 < 0x10 && mData2 < 0x10) { 985 // Null Pad, ignore 986 return String.format("[%d]Null: %02x %02x", mType, mData1, mData2); 987 } 988 989 int ctrlCode = getCtrlCode(); 990 if (ctrlCode != INVALID) { 991 return String.format("[%d]%s", mType, ctrlCodeToString(ctrlCode)); 992 } 993 994 int tabOffset = getTabOffset(); 995 if (tabOffset > 0) { 996 return String.format("[%d]Tab%d", mType, tabOffset); 997 } 998 999 PAC pac = getPAC(); 1000 if (pac != null) { 1001 return String.format("[%d]PAC: %s", mType, pac.toString()); 1002 } 1003 1004 StyleCode m = getMidRow(); 1005 if (m != null) { 1006 return String.format("[%d]Mid-row: %s", mType, m.toString()); 1007 } 1008 1009 if (isDisplayableChar()) { 1010 return String.format("[%d]Displayable: %s (%02x %02x)", 1011 mType, getDisplayText(), mData1, mData2); 1012 } 1013 1014 return String.format("[%d]Invalid: %02x %02x", mType, mData1, mData2); 1015 } 1016 } 1017} 1018 1019/** 1020 * Widget capable of rendering CEA-608 closed captions. 1021 * 1022 * @hide 1023 */ 1024class ClosedCaptionWidget extends ViewGroup implements 1025 SubtitleTrack.RenderingWidget, 1026 CCParser.DisplayListener { 1027 private static final String TAG = "ClosedCaptionWidget"; 1028 1029 private static final Rect mTextBounds = new Rect(); 1030 private static final String mDummyText = "1234567890123456789012345678901234"; 1031 private static final CaptionStyle DEFAULT_CAPTION_STYLE = CaptionStyle.DEFAULT; 1032 1033 /** Captioning manager, used to obtain and track caption properties. */ 1034 private final CaptioningManager mManager; 1035 1036 /** Callback for rendering changes. */ 1037 private OnChangedListener mListener; 1038 1039 /** Current caption style. */ 1040 private CaptionStyle mCaptionStyle; 1041 1042 /* Closed caption layout. */ 1043 private CCLayout mClosedCaptionLayout; 1044 1045 /** Whether a caption style change listener is registered. */ 1046 private boolean mHasChangeListener; 1047 1048 public ClosedCaptionWidget(Context context) { 1049 this(context, null); 1050 } 1051 1052 public ClosedCaptionWidget(Context context, AttributeSet attrs) { 1053 this(context, null, 0); 1054 } 1055 1056 public ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyle) { 1057 super(context, attrs, defStyle); 1058 1059 // Cannot render text over video when layer type is hardware. 1060 setLayerType(View.LAYER_TYPE_SOFTWARE, null); 1061 1062 mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); 1063 mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(mManager.getUserStyle()); 1064 1065 mClosedCaptionLayout = new CCLayout(context); 1066 mClosedCaptionLayout.setCaptionStyle(mCaptionStyle); 1067 addView(mClosedCaptionLayout, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 1068 1069 requestLayout(); 1070 } 1071 1072 @Override 1073 public void setOnChangedListener(OnChangedListener listener) { 1074 mListener = listener; 1075 } 1076 1077 @Override 1078 public void setSize(int width, int height) { 1079 final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); 1080 final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); 1081 1082 measure(widthSpec, heightSpec); 1083 layout(0, 0, width, height); 1084 } 1085 1086 @Override 1087 public void setVisible(boolean visible) { 1088 if (visible) { 1089 setVisibility(View.VISIBLE); 1090 } else { 1091 setVisibility(View.GONE); 1092 } 1093 1094 manageChangeListener(); 1095 } 1096 1097 @Override 1098 public void onAttachedToWindow() { 1099 super.onAttachedToWindow(); 1100 1101 manageChangeListener(); 1102 } 1103 1104 @Override 1105 public void onDetachedFromWindow() { 1106 super.onDetachedFromWindow(); 1107 1108 manageChangeListener(); 1109 } 1110 1111 @Override 1112 public void onDisplayChanged(SpannableStringBuilder[] styledTexts) { 1113 mClosedCaptionLayout.update(styledTexts); 1114 1115 if (mListener != null) { 1116 mListener.onChanged(this); 1117 } 1118 } 1119 1120 @Override 1121 public CaptionStyle getCaptionStyle() { 1122 return mCaptionStyle; 1123 } 1124 1125 @Override 1126 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1127 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1128 mClosedCaptionLayout.measure(widthMeasureSpec, heightMeasureSpec); 1129 } 1130 1131 @Override 1132 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1133 mClosedCaptionLayout.layout(l, t, r, b); 1134 } 1135 1136 /** 1137 * Manages whether this renderer is listening for caption style changes. 1138 */ 1139 private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() { 1140 @Override 1141 public void onUserStyleChanged(CaptionStyle userStyle) { 1142 mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(userStyle); 1143 mClosedCaptionLayout.setCaptionStyle(mCaptionStyle); 1144 } 1145 }; 1146 1147 private void manageChangeListener() { 1148 final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE; 1149 if (mHasChangeListener != needsListener) { 1150 mHasChangeListener = needsListener; 1151 1152 if (needsListener) { 1153 mManager.addCaptioningChangeListener(mCaptioningListener); 1154 } else { 1155 mManager.removeCaptioningChangeListener(mCaptioningListener); 1156 } 1157 } 1158 } 1159 1160 private static class CCLineBox extends TextView { 1161 private static final float FONT_PADDING_RATIO = 0.75f; 1162 1163 CCLineBox(Context context) { 1164 super(context); 1165 setGravity(Gravity.CENTER); 1166 setBackgroundColor(Color.TRANSPARENT); 1167 setTextColor(Color.WHITE); 1168 setTypeface(Typeface.MONOSPACE); 1169 setVisibility(View.INVISIBLE); 1170 } 1171 1172 void setCaptionStyle(CaptionStyle captionStyle) { 1173 setTextColor(captionStyle.foregroundColor); 1174 // TODO: edge color? 1175 } 1176 1177 @Override 1178 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1179 float fontSize = MeasureSpec.getSize(heightMeasureSpec) 1180 * FONT_PADDING_RATIO; 1181 setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize); 1182 1183 // set font scale in the X direction to match the required width 1184 setScaleX(1.0f); 1185 getPaint().getTextBounds(mDummyText, 0, mDummyText.length(), mTextBounds); 1186 float actualTextWidth = mTextBounds.width(); 1187 float requiredTextWidth = MeasureSpec.getSize(widthMeasureSpec); 1188 setScaleX(requiredTextWidth / actualTextWidth); 1189 1190 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1191 } 1192 } 1193 1194 private static class CCLayout extends LinearLayout { 1195 private static final int MAX_ROWS = CCParser.MAX_ROWS; 1196 private static final float SAFE_AREA_RATIO = 0.9f; 1197 1198 private final CCLineBox[] mLineBoxes = new CCLineBox[MAX_ROWS]; 1199 1200 CCLayout(Context context) { 1201 super(context); 1202 setGravity(Gravity.START); 1203 setOrientation(LinearLayout.VERTICAL); 1204 for (int i = 0; i < MAX_ROWS; i++) { 1205 mLineBoxes[i] = new CCLineBox(getContext()); 1206 addView(mLineBoxes[i], LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 1207 } 1208 } 1209 1210 void setCaptionStyle(CaptionStyle captionStyle) { 1211 for (int i = 0; i < MAX_ROWS; i++) { 1212 mLineBoxes[i].setCaptionStyle(captionStyle); 1213 } 1214 } 1215 1216 void update(SpannableStringBuilder[] textBuffer) { 1217 for (int i = 0; i < MAX_ROWS; i++) { 1218 if (textBuffer[i] != null) { 1219 mLineBoxes[i].setText(textBuffer[i]); 1220 mLineBoxes[i].setVisibility(View.VISIBLE); 1221 } else { 1222 mLineBoxes[i].setVisibility(View.INVISIBLE); 1223 } 1224 } 1225 } 1226 1227 @Override 1228 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1229 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1230 1231 int safeWidth = getMeasuredWidth(); 1232 int safeHeight = getMeasuredHeight(); 1233 1234 // CEA-608 assumes 4:3 video 1235 if (safeWidth * 3 >= safeHeight * 4) { 1236 safeWidth = safeHeight * 4 / 3; 1237 } else { 1238 safeHeight = safeWidth * 3 / 4; 1239 } 1240 safeWidth *= SAFE_AREA_RATIO; 1241 safeHeight *= SAFE_AREA_RATIO; 1242 1243 int lineHeight = safeHeight / MAX_ROWS; 1244 int lineHeightMeasureSpec = MeasureSpec.makeMeasureSpec( 1245 lineHeight, MeasureSpec.EXACTLY); 1246 int lineWidthMeasureSpec = MeasureSpec.makeMeasureSpec( 1247 safeWidth, MeasureSpec.EXACTLY); 1248 1249 for (int i = 0; i < MAX_ROWS; i++) { 1250 mLineBoxes[i].measure(lineWidthMeasureSpec, lineHeightMeasureSpec); 1251 } 1252 } 1253 1254 @Override 1255 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1256 // safe caption area 1257 int viewPortWidth = r - l; 1258 int viewPortHeight = b - t; 1259 int safeWidth, safeHeight; 1260 // CEA-608 assumes 4:3 video 1261 if (viewPortWidth * 3 >= viewPortHeight * 4) { 1262 safeWidth = viewPortHeight * 4 / 3; 1263 safeHeight = viewPortHeight; 1264 } else { 1265 safeWidth = viewPortWidth; 1266 safeHeight = viewPortWidth * 3 / 4; 1267 } 1268 safeWidth *= SAFE_AREA_RATIO; 1269 safeHeight *= SAFE_AREA_RATIO; 1270 int left = (viewPortWidth - safeWidth) / 2; 1271 int top = (viewPortHeight - safeHeight) / 2; 1272 1273 for (int i = 0; i < MAX_ROWS; i++) { 1274 mLineBoxes[i].layout( 1275 left, 1276 top + safeHeight * i / MAX_ROWS, 1277 left + safeWidth, 1278 top + safeHeight * (i + 1) / MAX_ROWS); 1279 } 1280 } 1281 } 1282}; 1283