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