| /* |
| * Copyright 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.media.subtitle; |
| |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.Paint; |
| import android.graphics.Rect; |
| import android.graphics.Typeface; |
| import android.media.MediaFormat; |
| import android.text.Spannable; |
| import android.text.SpannableStringBuilder; |
| import android.text.TextPaint; |
| import android.text.style.CharacterStyle; |
| import android.text.style.StyleSpan; |
| import android.text.style.UnderlineSpan; |
| import android.text.style.UpdateAppearance; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.util.TypedValue; |
| import android.view.Gravity; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.accessibility.CaptioningManager; |
| import android.view.accessibility.CaptioningManager.CaptionStyle; |
| import android.view.accessibility.CaptioningManager.CaptioningChangeListener; |
| import android.widget.LinearLayout; |
| import android.widget.TextView; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Vector; |
| |
| // Note: This is forked from android.media.ClosedCaptionRenderer since P |
| public class ClosedCaptionRenderer extends SubtitleController.Renderer { |
| private final Context mContext; |
| private Cea608CCWidget mCCWidget; |
| |
| public ClosedCaptionRenderer(Context context) { |
| mContext = context; |
| } |
| |
| @Override |
| public boolean supports(MediaFormat format) { |
| if (format.containsKey(MediaFormat.KEY_MIME)) { |
| String mimeType = format.getString(MediaFormat.KEY_MIME); |
| return MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType); |
| } |
| return false; |
| } |
| |
| @Override |
| public SubtitleTrack createTrack(MediaFormat format) { |
| String mimeType = format.getString(MediaFormat.KEY_MIME); |
| if (MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType)) { |
| if (mCCWidget == null) { |
| mCCWidget = new Cea608CCWidget(mContext); |
| } |
| return new Cea608CaptionTrack(mCCWidget, format); |
| } |
| throw new RuntimeException("No matching format: " + format.toString()); |
| } |
| } |
| |
| class Cea608CaptionTrack extends SubtitleTrack { |
| private final Cea608CCParser mCCParser; |
| private final Cea608CCWidget mRenderingWidget; |
| |
| Cea608CaptionTrack(Cea608CCWidget renderingWidget, MediaFormat format) { |
| super(format); |
| |
| mRenderingWidget = renderingWidget; |
| mCCParser = new Cea608CCParser(mRenderingWidget); |
| } |
| |
| @Override |
| public void onData(byte[] data, boolean eos, long runID) { |
| mCCParser.parse(data); |
| } |
| |
| @Override |
| public RenderingWidget getRenderingWidget() { |
| return mRenderingWidget; |
| } |
| |
| @Override |
| public void updateView(Vector<Cue> activeCues) { |
| // Overriding with NO-OP, CC rendering by-passes this |
| } |
| } |
| |
| /** |
| * Abstract widget class to render a closed caption track. |
| */ |
| abstract class ClosedCaptionWidget extends ViewGroup implements SubtitleTrack.RenderingWidget { |
| |
| interface ClosedCaptionLayout { |
| void setCaptionStyle(CaptionStyle captionStyle); |
| void setFontScale(float scale); |
| } |
| |
| private static final CaptionStyle DEFAULT_CAPTION_STYLE = CaptionStyle.DEFAULT; |
| |
| /** Captioning manager, used to obtain and track caption properties. */ |
| private final CaptioningManager mManager; |
| |
| /** Current caption style. */ |
| protected CaptionStyle mCaptionStyle; |
| |
| /** Callback for rendering changes. */ |
| protected OnChangedListener mListener; |
| |
| /** Concrete layout of CC. */ |
| protected ClosedCaptionLayout mClosedCaptionLayout; |
| |
| /** Whether a caption style change listener is registered. */ |
| private boolean mHasChangeListener; |
| |
| public ClosedCaptionWidget(Context context) { |
| this(context, null); |
| } |
| |
| public ClosedCaptionWidget(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyle) { |
| this(context, attrs, defStyle, 0); |
| } |
| |
| public ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyleAttr, |
| int defStyleRes) { |
| super(context, attrs, defStyleAttr, defStyleRes); |
| |
| // Cannot render text over video when layer type is hardware. |
| setLayerType(View.LAYER_TYPE_SOFTWARE, null); |
| |
| mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); |
| mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(mManager.getUserStyle()); |
| |
| mClosedCaptionLayout = createCaptionLayout(context); |
| mClosedCaptionLayout.setCaptionStyle(mCaptionStyle); |
| mClosedCaptionLayout.setFontScale(mManager.getFontScale()); |
| addView((ViewGroup) mClosedCaptionLayout, LayoutParams.MATCH_PARENT, |
| LayoutParams.MATCH_PARENT); |
| |
| requestLayout(); |
| } |
| |
| public abstract ClosedCaptionLayout createCaptionLayout(Context context); |
| |
| @Override |
| public void setOnChangedListener(OnChangedListener listener) { |
| mListener = listener; |
| } |
| |
| @Override |
| public void setSize(int width, int height) { |
| final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); |
| final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); |
| |
| measure(widthSpec, heightSpec); |
| layout(0, 0, width, height); |
| } |
| |
| @Override |
| public void setVisible(boolean visible) { |
| if (visible) { |
| setVisibility(View.VISIBLE); |
| } else { |
| setVisibility(View.GONE); |
| } |
| |
| manageChangeListener(); |
| } |
| |
| @Override |
| public void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| |
| manageChangeListener(); |
| } |
| |
| @Override |
| public void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| |
| manageChangeListener(); |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| ((ViewGroup) mClosedCaptionLayout).measure(widthMeasureSpec, heightMeasureSpec); |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int l, int t, int r, int b) { |
| ((ViewGroup) mClosedCaptionLayout).layout(l, t, r, b); |
| } |
| |
| /** |
| * Manages whether this renderer is listening for caption style changes. |
| */ |
| private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() { |
| @Override |
| public void onUserStyleChanged(CaptionStyle userStyle) { |
| mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(userStyle); |
| mClosedCaptionLayout.setCaptionStyle(mCaptionStyle); |
| } |
| |
| @Override |
| public void onFontScaleChanged(float fontScale) { |
| mClosedCaptionLayout.setFontScale(fontScale); |
| } |
| }; |
| |
| private void manageChangeListener() { |
| final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE; |
| if (mHasChangeListener != needsListener) { |
| mHasChangeListener = needsListener; |
| |
| if (needsListener) { |
| mManager.addCaptioningChangeListener(mCaptioningListener); |
| } else { |
| mManager.removeCaptioningChangeListener(mCaptioningListener); |
| } |
| } |
| } |
| } |
| |
| /** |
| * CCParser processes CEA-608 closed caption data. |
| * |
| * It calls back into OnDisplayChangedListener upon |
| * display change with styled text for rendering. |
| * |
| */ |
| class Cea608CCParser { |
| public static final int MAX_ROWS = 15; |
| public static final int MAX_COLS = 32; |
| |
| private static final String TAG = "Cea608CCParser"; |
| private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); |
| |
| private static final int INVALID = -1; |
| |
| // EIA-CEA-608: Table 70 - Control Codes |
| private static final int RCL = 0x20; |
| private static final int BS = 0x21; |
| private static final int AOF = 0x22; |
| private static final int AON = 0x23; |
| private static final int DER = 0x24; |
| private static final int RU2 = 0x25; |
| private static final int RU3 = 0x26; |
| private static final int RU4 = 0x27; |
| private static final int FON = 0x28; |
| private static final int RDC = 0x29; |
| private static final int TR = 0x2a; |
| private static final int RTD = 0x2b; |
| private static final int EDM = 0x2c; |
| private static final int CR = 0x2d; |
| private static final int ENM = 0x2e; |
| private static final int EOC = 0x2f; |
| |
| // Transparent Space |
| private static final char TS = '\u00A0'; |
| |
| // Captioning Modes |
| private static final int MODE_UNKNOWN = 0; |
| private static final int MODE_PAINT_ON = 1; |
| private static final int MODE_ROLL_UP = 2; |
| private static final int MODE_POP_ON = 3; |
| private static final int MODE_TEXT = 4; |
| |
| private final DisplayListener mListener; |
| |
| private int mMode = MODE_PAINT_ON; |
| private int mRollUpSize = 4; |
| private int mPrevCtrlCode = INVALID; |
| |
| private CCMemory mDisplay = new CCMemory(); |
| private CCMemory mNonDisplay = new CCMemory(); |
| private CCMemory mTextMem = new CCMemory(); |
| |
| Cea608CCParser(DisplayListener listener) { |
| mListener = listener; |
| } |
| |
| public void parse(byte[] data) { |
| CCData[] ccData = CCData.fromByteArray(data); |
| |
| for (int i = 0; i < ccData.length; i++) { |
| if (DEBUG) { |
| Log.d(TAG, ccData[i].toString()); |
| } |
| |
| if (handleCtrlCode(ccData[i]) |
| || handleTabOffsets(ccData[i]) |
| || handlePACCode(ccData[i]) |
| || handleMidRowCode(ccData[i])) { |
| continue; |
| } |
| |
| handleDisplayableChars(ccData[i]); |
| } |
| } |
| |
| interface DisplayListener { |
| void onDisplayChanged(SpannableStringBuilder[] styledTexts); |
| CaptionStyle getCaptionStyle(); |
| } |
| |
| private CCMemory getMemory() { |
| // get the CC memory to operate on for current mode |
| switch (mMode) { |
| case MODE_POP_ON: |
| return mNonDisplay; |
| case MODE_TEXT: |
| // TODO(chz): support only caption mode for now, |
| // in text mode, dump everything to text mem. |
| return mTextMem; |
| case MODE_PAINT_ON: |
| case MODE_ROLL_UP: |
| return mDisplay; |
| default: |
| Log.w(TAG, "unrecoginized mode: " + mMode); |
| } |
| return mDisplay; |
| } |
| |
| private boolean handleDisplayableChars(CCData ccData) { |
| if (!ccData.isDisplayableChar()) { |
| return false; |
| } |
| |
| // Extended char includes 1 automatic backspace |
| if (ccData.isExtendedChar()) { |
| getMemory().bs(); |
| } |
| |
| getMemory().writeText(ccData.getDisplayText()); |
| |
| if (mMode == MODE_PAINT_ON || mMode == MODE_ROLL_UP) { |
| updateDisplay(); |
| } |
| |
| return true; |
| } |
| |
| private boolean handleMidRowCode(CCData ccData) { |
| StyleCode m = ccData.getMidRow(); |
| if (m != null) { |
| getMemory().writeMidRowCode(m); |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean handlePACCode(CCData ccData) { |
| PAC pac = ccData.getPAC(); |
| |
| if (pac != null) { |
| if (mMode == MODE_ROLL_UP) { |
| getMemory().moveBaselineTo(pac.getRow(), mRollUpSize); |
| } |
| getMemory().writePAC(pac); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private boolean handleTabOffsets(CCData ccData) { |
| int tabs = ccData.getTabOffset(); |
| |
| if (tabs > 0) { |
| getMemory().tab(tabs); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private boolean handleCtrlCode(CCData ccData) { |
| int ctrlCode = ccData.getCtrlCode(); |
| |
| if (mPrevCtrlCode != INVALID && mPrevCtrlCode == ctrlCode) { |
| // discard double ctrl codes (but if there's a 3rd one, we still take that) |
| mPrevCtrlCode = INVALID; |
| return true; |
| } |
| |
| switch(ctrlCode) { |
| case RCL: |
| // select pop-on style |
| mMode = MODE_POP_ON; |
| break; |
| case BS: |
| getMemory().bs(); |
| break; |
| case DER: |
| getMemory().der(); |
| break; |
| case RU2: |
| case RU3: |
| case RU4: |
| mRollUpSize = (ctrlCode - 0x23); |
| // erase memory if currently in other style |
| if (mMode != MODE_ROLL_UP) { |
| mDisplay.erase(); |
| mNonDisplay.erase(); |
| } |
| // select roll-up style |
| mMode = MODE_ROLL_UP; |
| break; |
| case FON: |
| Log.i(TAG, "Flash On"); |
| break; |
| case RDC: |
| // select paint-on style |
| mMode = MODE_PAINT_ON; |
| break; |
| case TR: |
| mMode = MODE_TEXT; |
| mTextMem.erase(); |
| break; |
| case RTD: |
| mMode = MODE_TEXT; |
| break; |
| case EDM: |
| // erase display memory |
| mDisplay.erase(); |
| updateDisplay(); |
| break; |
| case CR: |
| if (mMode == MODE_ROLL_UP) { |
| getMemory().rollUp(mRollUpSize); |
| } else { |
| getMemory().cr(); |
| } |
| if (mMode == MODE_ROLL_UP) { |
| updateDisplay(); |
| } |
| break; |
| case ENM: |
| // erase non-display memory |
| mNonDisplay.erase(); |
| break; |
| case EOC: |
| // swap display/non-display memory |
| swapMemory(); |
| // switch to pop-on style |
| mMode = MODE_POP_ON; |
| updateDisplay(); |
| break; |
| case INVALID: |
| default: |
| mPrevCtrlCode = INVALID; |
| return false; |
| } |
| |
| mPrevCtrlCode = ctrlCode; |
| |
| // handled |
| return true; |
| } |
| |
| private void updateDisplay() { |
| if (mListener != null) { |
| CaptionStyle captionStyle = mListener.getCaptionStyle(); |
| mListener.onDisplayChanged(mDisplay.getStyledText(captionStyle)); |
| } |
| } |
| |
| private void swapMemory() { |
| CCMemory temp = mDisplay; |
| mDisplay = mNonDisplay; |
| mNonDisplay = temp; |
| } |
| |
| private static class StyleCode { |
| static final int COLOR_WHITE = 0; |
| static final int COLOR_GREEN = 1; |
| static final int COLOR_BLUE = 2; |
| static final int COLOR_CYAN = 3; |
| static final int COLOR_RED = 4; |
| static final int COLOR_YELLOW = 5; |
| static final int COLOR_MAGENTA = 6; |
| static final int COLOR_INVALID = 7; |
| |
| static final int STYLE_ITALICS = 0x00000001; |
| static final int STYLE_UNDERLINE = 0x00000002; |
| |
| static final String[] mColorMap = { |
| "WHITE", "GREEN", "BLUE", "CYAN", "RED", "YELLOW", "MAGENTA", "INVALID" |
| }; |
| |
| final int mStyle; |
| final int mColor; |
| |
| static StyleCode fromByte(byte data2) { |
| int style = 0; |
| int color = (data2 >> 1) & 0x7; |
| |
| if ((data2 & 0x1) != 0) { |
| style |= STYLE_UNDERLINE; |
| } |
| |
| if (color == COLOR_INVALID) { |
| // WHITE ITALICS |
| color = COLOR_WHITE; |
| style |= STYLE_ITALICS; |
| } |
| |
| return new StyleCode(style, color); |
| } |
| |
| StyleCode(int style, int color) { |
| mStyle = style; |
| mColor = color; |
| } |
| |
| boolean isItalics() { |
| return (mStyle & STYLE_ITALICS) != 0; |
| } |
| |
| boolean isUnderline() { |
| return (mStyle & STYLE_UNDERLINE) != 0; |
| } |
| |
| int getColor() { |
| return mColor; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder str = new StringBuilder(); |
| str.append("{"); |
| str.append(mColorMap[mColor]); |
| if ((mStyle & STYLE_ITALICS) != 0) { |
| str.append(", ITALICS"); |
| } |
| if ((mStyle & STYLE_UNDERLINE) != 0) { |
| str.append(", UNDERLINE"); |
| } |
| str.append("}"); |
| |
| return str.toString(); |
| } |
| } |
| |
| private static class PAC extends StyleCode { |
| final int mRow; |
| final int mCol; |
| |
| static PAC fromBytes(byte data1, byte data2) { |
| int[] rowTable = {11, 1, 3, 12, 14, 5, 7, 9}; |
| int row = rowTable[data1 & 0x07] + ((data2 & 0x20) >> 5); |
| int style = 0; |
| if ((data2 & 1) != 0) { |
| style |= STYLE_UNDERLINE; |
| } |
| if ((data2 & 0x10) != 0) { |
| // indent code |
| int indent = (data2 >> 1) & 0x7; |
| return new PAC(row, indent * 4, style, COLOR_WHITE); |
| } else { |
| // style code |
| int color = (data2 >> 1) & 0x7; |
| |
| if (color == COLOR_INVALID) { |
| // WHITE ITALICS |
| color = COLOR_WHITE; |
| style |= STYLE_ITALICS; |
| } |
| return new PAC(row, -1, style, color); |
| } |
| } |
| |
| PAC(int row, int col, int style, int color) { |
| super(style, color); |
| mRow = row; |
| mCol = col; |
| } |
| |
| boolean isIndentPAC() { |
| return (mCol >= 0); |
| } |
| |
| int getRow() { |
| return mRow; |
| } |
| |
| int getCol() { |
| return mCol; |
| } |
| |
| @Override |
| public String toString() { |
| return String.format("{%d, %d}, %s", |
| mRow, mCol, super.toString()); |
| } |
| } |
| |
| /** |
| * Mutable version of BackgroundSpan to facilitate text rendering with edge styles. |
| */ |
| public static class MutableBackgroundColorSpan extends CharacterStyle |
| implements UpdateAppearance { |
| private int mColor; |
| |
| public MutableBackgroundColorSpan(int color) { |
| mColor = color; |
| } |
| |
| public void setBackgroundColor(int color) { |
| mColor = color; |
| } |
| |
| public int getBackgroundColor() { |
| return mColor; |
| } |
| |
| @Override |
| public void updateDrawState(TextPaint ds) { |
| ds.bgColor = mColor; |
| } |
| } |
| |
| /* CCLineBuilder keeps track of displayable chars, as well as |
| * MidRow styles and PACs, for a single line of CC memory. |
| * |
| * It generates styled text via getStyledText() method. |
| */ |
| private static class CCLineBuilder { |
| private final StringBuilder mDisplayChars; |
| private final StyleCode[] mMidRowStyles; |
| private final StyleCode[] mPACStyles; |
| |
| CCLineBuilder(String str) { |
| mDisplayChars = new StringBuilder(str); |
| mMidRowStyles = new StyleCode[mDisplayChars.length()]; |
| mPACStyles = new StyleCode[mDisplayChars.length()]; |
| } |
| |
| void setCharAt(int index, char ch) { |
| mDisplayChars.setCharAt(index, ch); |
| mMidRowStyles[index] = null; |
| } |
| |
| void setMidRowAt(int index, StyleCode m) { |
| mDisplayChars.setCharAt(index, ' '); |
| mMidRowStyles[index] = m; |
| } |
| |
| void setPACAt(int index, PAC pac) { |
| mPACStyles[index] = pac; |
| } |
| |
| char charAt(int index) { |
| return mDisplayChars.charAt(index); |
| } |
| |
| int length() { |
| return mDisplayChars.length(); |
| } |
| |
| void applyStyleSpan( |
| SpannableStringBuilder styledText, |
| StyleCode s, int start, int end) { |
| if (s.isItalics()) { |
| styledText.setSpan( |
| new StyleSpan(android.graphics.Typeface.ITALIC), |
| start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
| } |
| if (s.isUnderline()) { |
| styledText.setSpan( |
| new UnderlineSpan(), |
| start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
| } |
| } |
| |
| SpannableStringBuilder getStyledText(CaptionStyle captionStyle) { |
| SpannableStringBuilder styledText = new SpannableStringBuilder(mDisplayChars); |
| int start = -1, next = 0; |
| int styleStart = -1; |
| StyleCode curStyle = null; |
| while (next < mDisplayChars.length()) { |
| StyleCode newStyle = null; |
| if (mMidRowStyles[next] != null) { |
| // apply mid-row style change |
| newStyle = mMidRowStyles[next]; |
| } else if (mPACStyles[next] != null |
| && (styleStart < 0 || start < 0)) { |
| // apply PAC style change, only if: |
| // 1. no style set, or |
| // 2. style set, but prev char is none-displayable |
| newStyle = mPACStyles[next]; |
| } |
| if (newStyle != null) { |
| curStyle = newStyle; |
| if (styleStart >= 0 && start >= 0) { |
| applyStyleSpan(styledText, newStyle, styleStart, next); |
| } |
| styleStart = next; |
| } |
| |
| if (mDisplayChars.charAt(next) != TS) { |
| if (start < 0) { |
| start = next; |
| } |
| } else if (start >= 0) { |
| int expandedStart = mDisplayChars.charAt(start) == ' ' ? start : start - 1; |
| int expandedEnd = mDisplayChars.charAt(next - 1) == ' ' ? next : next + 1; |
| styledText.setSpan( |
| new MutableBackgroundColorSpan(captionStyle.backgroundColor), |
| expandedStart, expandedEnd, |
| Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); |
| if (styleStart >= 0) { |
| applyStyleSpan(styledText, curStyle, styleStart, expandedEnd); |
| } |
| start = -1; |
| } |
| next++; |
| } |
| |
| return styledText; |
| } |
| } |
| |
| /* |
| * CCMemory models a console-style display. |
| */ |
| private static class CCMemory { |
| private final String mBlankLine; |
| private final CCLineBuilder[] mLines = new CCLineBuilder[MAX_ROWS + 2]; |
| private int mRow; |
| private int mCol; |
| |
| CCMemory() { |
| char[] blank = new char[MAX_COLS + 2]; |
| Arrays.fill(blank, TS); |
| mBlankLine = new String(blank); |
| } |
| |
| void erase() { |
| // erase all lines |
| for (int i = 0; i < mLines.length; i++) { |
| mLines[i] = null; |
| } |
| mRow = MAX_ROWS; |
| mCol = 1; |
| } |
| |
| void der() { |
| if (mLines[mRow] != null) { |
| for (int i = 0; i < mCol; i++) { |
| if (mLines[mRow].charAt(i) != TS) { |
| for (int j = mCol; j < mLines[mRow].length(); j++) { |
| mLines[j].setCharAt(j, TS); |
| } |
| return; |
| } |
| } |
| mLines[mRow] = null; |
| } |
| } |
| |
| void tab(int tabs) { |
| moveCursorByCol(tabs); |
| } |
| |
| void bs() { |
| moveCursorByCol(-1); |
| if (mLines[mRow] != null) { |
| mLines[mRow].setCharAt(mCol, TS); |
| if (mCol == MAX_COLS - 1) { |
| // Spec recommendation: |
| // if cursor was at col 32, move cursor |
| // back to col 31 and erase both col 31&32 |
| mLines[mRow].setCharAt(MAX_COLS, TS); |
| } |
| } |
| } |
| |
| void cr() { |
| moveCursorTo(mRow + 1, 1); |
| } |
| |
| void rollUp(int windowSize) { |
| int i; |
| for (i = 0; i <= mRow - windowSize; i++) { |
| mLines[i] = null; |
| } |
| int startRow = mRow - windowSize + 1; |
| if (startRow < 1) { |
| startRow = 1; |
| } |
| for (i = startRow; i < mRow; i++) { |
| mLines[i] = mLines[i + 1]; |
| } |
| for (i = mRow; i < mLines.length; i++) { |
| // clear base row |
| mLines[i] = null; |
| } |
| // default to col 1, in case PAC is not sent |
| mCol = 1; |
| } |
| |
| void writeText(String text) { |
| for (int i = 0; i < text.length(); i++) { |
| getLineBuffer(mRow).setCharAt(mCol, text.charAt(i)); |
| moveCursorByCol(1); |
| } |
| } |
| |
| void writeMidRowCode(StyleCode m) { |
| getLineBuffer(mRow).setMidRowAt(mCol, m); |
| moveCursorByCol(1); |
| } |
| |
| void writePAC(PAC pac) { |
| if (pac.isIndentPAC()) { |
| moveCursorTo(pac.getRow(), pac.getCol()); |
| } else { |
| moveCursorTo(pac.getRow(), 1); |
| } |
| getLineBuffer(mRow).setPACAt(mCol, pac); |
| } |
| |
| SpannableStringBuilder[] getStyledText(CaptionStyle captionStyle) { |
| ArrayList<SpannableStringBuilder> rows = new ArrayList<>(MAX_ROWS); |
| for (int i = 1; i <= MAX_ROWS; i++) { |
| rows.add(mLines[i] != null ? |
| mLines[i].getStyledText(captionStyle) : null); |
| } |
| return rows.toArray(new SpannableStringBuilder[MAX_ROWS]); |
| } |
| |
| private static int clamp(int x, int min, int max) { |
| return x < min ? min : (x > max ? max : x); |
| } |
| |
| private void moveCursorTo(int row, int col) { |
| mRow = clamp(row, 1, MAX_ROWS); |
| mCol = clamp(col, 1, MAX_COLS); |
| } |
| |
| private void moveCursorToRow(int row) { |
| mRow = clamp(row, 1, MAX_ROWS); |
| } |
| |
| private void moveCursorByCol(int col) { |
| mCol = clamp(mCol + col, 1, MAX_COLS); |
| } |
| |
| private void moveBaselineTo(int baseRow, int windowSize) { |
| if (mRow == baseRow) { |
| return; |
| } |
| int actualWindowSize = windowSize; |
| if (baseRow < actualWindowSize) { |
| actualWindowSize = baseRow; |
| } |
| if (mRow < actualWindowSize) { |
| actualWindowSize = mRow; |
| } |
| |
| int i; |
| if (baseRow < mRow) { |
| // copy from bottom to top row |
| for (i = actualWindowSize - 1; i >= 0; i--) { |
| mLines[baseRow - i] = mLines[mRow - i]; |
| } |
| } else { |
| // copy from top to bottom row |
| for (i = 0; i < actualWindowSize; i++) { |
| mLines[baseRow - i] = mLines[mRow - i]; |
| } |
| } |
| // clear rest of the rows |
| for (i = 0; i <= baseRow - windowSize; i++) { |
| mLines[i] = null; |
| } |
| for (i = baseRow + 1; i < mLines.length; i++) { |
| mLines[i] = null; |
| } |
| } |
| |
| private CCLineBuilder getLineBuffer(int row) { |
| if (mLines[row] == null) { |
| mLines[row] = new CCLineBuilder(mBlankLine); |
| } |
| return mLines[row]; |
| } |
| } |
| |
| /* |
| * CCData parses the raw CC byte pair into displayable chars, |
| * misc control codes, Mid-Row or Preamble Address Codes. |
| */ |
| private static class CCData { |
| private final byte mType; |
| private final byte mData1; |
| private final byte mData2; |
| |
| private static final String[] mCtrlCodeMap = { |
| "RCL", "BS" , "AOF", "AON", |
| "DER", "RU2", "RU3", "RU4", |
| "FON", "RDC", "TR" , "RTD", |
| "EDM", "CR" , "ENM", "EOC", |
| }; |
| |
| private static final String[] mSpecialCharMap = { |
| "\u00AE", |
| "\u00B0", |
| "\u00BD", |
| "\u00BF", |
| "\u2122", |
| "\u00A2", |
| "\u00A3", |
| "\u266A", // Eighth note |
| "\u00E0", |
| "\u00A0", // Transparent space |
| "\u00E8", |
| "\u00E2", |
| "\u00EA", |
| "\u00EE", |
| "\u00F4", |
| "\u00FB", |
| }; |
| |
| private static final String[] mSpanishCharMap = { |
| // Spanish and misc chars |
| "\u00C1", // A |
| "\u00C9", // E |
| "\u00D3", // I |
| "\u00DA", // O |
| "\u00DC", // U |
| "\u00FC", // u |
| "\u2018", // opening single quote |
| "\u00A1", // inverted exclamation mark |
| "*", |
| "'", |
| "\u2014", // em dash |
| "\u00A9", // Copyright |
| "\u2120", // Servicemark |
| "\u2022", // round bullet |
| "\u201C", // opening double quote |
| "\u201D", // closing double quote |
| // French |
| "\u00C0", |
| "\u00C2", |
| "\u00C7", |
| "\u00C8", |
| "\u00CA", |
| "\u00CB", |
| "\u00EB", |
| "\u00CE", |
| "\u00CF", |
| "\u00EF", |
| "\u00D4", |
| "\u00D9", |
| "\u00F9", |
| "\u00DB", |
| "\u00AB", |
| "\u00BB" |
| }; |
| |
| private static final String[] mProtugueseCharMap = { |
| // Portuguese |
| "\u00C3", |
| "\u00E3", |
| "\u00CD", |
| "\u00CC", |
| "\u00EC", |
| "\u00D2", |
| "\u00F2", |
| "\u00D5", |
| "\u00F5", |
| "{", |
| "}", |
| "\\", |
| "^", |
| "_", |
| "|", |
| "~", |
| // German and misc chars |
| "\u00C4", |
| "\u00E4", |
| "\u00D6", |
| "\u00F6", |
| "\u00DF", |
| "\u00A5", |
| "\u00A4", |
| "\u2502", // vertical bar |
| "\u00C5", |
| "\u00E5", |
| "\u00D8", |
| "\u00F8", |
| "\u250C", // top-left corner |
| "\u2510", // top-right corner |
| "\u2514", // lower-left corner |
| "\u2518", // lower-right corner |
| }; |
| |
| static CCData[] fromByteArray(byte[] data) { |
| CCData[] ccData = new CCData[data.length / 3]; |
| |
| for (int i = 0; i < ccData.length; i++) { |
| ccData[i] = new CCData( |
| data[i * 3], |
| data[i * 3 + 1], |
| data[i * 3 + 2]); |
| } |
| |
| return ccData; |
| } |
| |
| CCData(byte type, byte data1, byte data2) { |
| mType = type; |
| mData1 = data1; |
| mData2 = data2; |
| } |
| |
| int getCtrlCode() { |
| if ((mData1 == 0x14 || mData1 == 0x1c) |
| && mData2 >= 0x20 && mData2 <= 0x2f) { |
| return mData2; |
| } |
| return INVALID; |
| } |
| |
| StyleCode getMidRow() { |
| // only support standard Mid-row codes, ignore |
| // optional background/foreground mid-row codes |
| if ((mData1 == 0x11 || mData1 == 0x19) |
| && mData2 >= 0x20 && mData2 <= 0x2f) { |
| return StyleCode.fromByte(mData2); |
| } |
| return null; |
| } |
| |
| PAC getPAC() { |
| if ((mData1 & 0x70) == 0x10 |
| && (mData2 & 0x40) == 0x40 |
| && ((mData1 & 0x07) != 0 || (mData2 & 0x20) == 0)) { |
| return PAC.fromBytes(mData1, mData2); |
| } |
| return null; |
| } |
| |
| int getTabOffset() { |
| if ((mData1 == 0x17 || mData1 == 0x1f) |
| && mData2 >= 0x21 && mData2 <= 0x23) { |
| return mData2 & 0x3; |
| } |
| return 0; |
| } |
| |
| boolean isDisplayableChar() { |
| return isBasicChar() || isSpecialChar() || isExtendedChar(); |
| } |
| |
| String getDisplayText() { |
| String str = getBasicChars(); |
| |
| if (str == null) { |
| str = getSpecialChar(); |
| |
| if (str == null) { |
| str = getExtendedChar(); |
| } |
| } |
| |
| return str; |
| } |
| |
| private String ctrlCodeToString(int ctrlCode) { |
| return mCtrlCodeMap[ctrlCode - 0x20]; |
| } |
| |
| private boolean isBasicChar() { |
| return mData1 >= 0x20 && mData1 <= 0x7f; |
| } |
| |
| private boolean isSpecialChar() { |
| return ((mData1 == 0x11 || mData1 == 0x19) |
| && mData2 >= 0x30 && mData2 <= 0x3f); |
| } |
| |
| private boolean isExtendedChar() { |
| return ((mData1 == 0x12 || mData1 == 0x1A |
| || mData1 == 0x13 || mData1 == 0x1B) |
| && mData2 >= 0x20 && mData2 <= 0x3f); |
| } |
| |
| private char getBasicChar(byte data) { |
| char c; |
| // replace the non-ASCII ones |
| switch (data) { |
| case 0x2A: c = '\u00E1'; break; |
| case 0x5C: c = '\u00E9'; break; |
| case 0x5E: c = '\u00ED'; break; |
| case 0x5F: c = '\u00F3'; break; |
| case 0x60: c = '\u00FA'; break; |
| case 0x7B: c = '\u00E7'; break; |
| case 0x7C: c = '\u00F7'; break; |
| case 0x7D: c = '\u00D1'; break; |
| case 0x7E: c = '\u00F1'; break; |
| case 0x7F: c = '\u2588'; break; // Full block |
| default: c = (char) data; break; |
| } |
| return c; |
| } |
| |
| private String getBasicChars() { |
| if (mData1 >= 0x20 && mData1 <= 0x7f) { |
| StringBuilder builder = new StringBuilder(2); |
| builder.append(getBasicChar(mData1)); |
| if (mData2 >= 0x20 && mData2 <= 0x7f) { |
| builder.append(getBasicChar(mData2)); |
| } |
| return builder.toString(); |
| } |
| |
| return null; |
| } |
| |
| private String getSpecialChar() { |
| if ((mData1 == 0x11 || mData1 == 0x19) |
| && mData2 >= 0x30 && mData2 <= 0x3f) { |
| return mSpecialCharMap[mData2 - 0x30]; |
| } |
| |
| return null; |
| } |
| |
| private String getExtendedChar() { |
| if ((mData1 == 0x12 || mData1 == 0x1A) |
| && mData2 >= 0x20 && mData2 <= 0x3f){ |
| // 1 Spanish/French char |
| return mSpanishCharMap[mData2 - 0x20]; |
| } else if ((mData1 == 0x13 || mData1 == 0x1B) |
| && mData2 >= 0x20 && mData2 <= 0x3f){ |
| // 1 Portuguese/German/Danish char |
| return mProtugueseCharMap[mData2 - 0x20]; |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public String toString() { |
| String str; |
| |
| if (mData1 < 0x10 && mData2 < 0x10) { |
| // Null Pad, ignore |
| return String.format("[%d]Null: %02x %02x", mType, mData1, mData2); |
| } |
| |
| int ctrlCode = getCtrlCode(); |
| if (ctrlCode != INVALID) { |
| return String.format("[%d]%s", mType, ctrlCodeToString(ctrlCode)); |
| } |
| |
| int tabOffset = getTabOffset(); |
| if (tabOffset > 0) { |
| return String.format("[%d]Tab%d", mType, tabOffset); |
| } |
| |
| PAC pac = getPAC(); |
| if (pac != null) { |
| return String.format("[%d]PAC: %s", mType, pac.toString()); |
| } |
| |
| StyleCode m = getMidRow(); |
| if (m != null) { |
| return String.format("[%d]Mid-row: %s", mType, m.toString()); |
| } |
| |
| if (isDisplayableChar()) { |
| return String.format("[%d]Displayable: %s (%02x %02x)", |
| mType, getDisplayText(), mData1, mData2); |
| } |
| |
| return String.format("[%d]Invalid: %02x %02x", mType, mData1, mData2); |
| } |
| } |
| } |
| |
| /** |
| * Widget capable of rendering CEA-608 closed captions. |
| */ |
| class Cea608CCWidget extends ClosedCaptionWidget implements Cea608CCParser.DisplayListener { |
| private static final Rect mTextBounds = new Rect(); |
| private static final String mDummyText = "1234567890123456789012345678901234"; |
| |
| public Cea608CCWidget(Context context) { |
| this(context, null); |
| } |
| |
| public Cea608CCWidget(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public Cea608CCWidget(Context context, AttributeSet attrs, int defStyle) { |
| this(context, attrs, defStyle, 0); |
| } |
| |
| public Cea608CCWidget(Context context, AttributeSet attrs, int defStyleAttr, |
| int defStyleRes) { |
| super(context, attrs, defStyleAttr, defStyleRes); |
| } |
| |
| @Override |
| public ClosedCaptionLayout createCaptionLayout(Context context) { |
| return new CCLayout(context); |
| } |
| |
| @Override |
| public void onDisplayChanged(SpannableStringBuilder[] styledTexts) { |
| ((CCLayout) mClosedCaptionLayout).update(styledTexts); |
| |
| if (mListener != null) { |
| mListener.onChanged(this); |
| } |
| } |
| |
| @Override |
| public CaptionStyle getCaptionStyle() { |
| return mCaptionStyle; |
| } |
| |
| private static class CCLineBox extends TextView { |
| private static final float FONT_PADDING_RATIO = 0.75f; |
| private static final float EDGE_OUTLINE_RATIO = 0.1f; |
| private static final float EDGE_SHADOW_RATIO = 0.05f; |
| private float mOutlineWidth; |
| private float mShadowRadius; |
| private float mShadowOffset; |
| |
| private int mTextColor = Color.WHITE; |
| private int mBgColor = Color.BLACK; |
| private int mEdgeType = CaptionStyle.EDGE_TYPE_NONE; |
| private int mEdgeColor = Color.TRANSPARENT; |
| |
| CCLineBox(Context context) { |
| super(context); |
| setGravity(Gravity.CENTER); |
| setBackgroundColor(Color.TRANSPARENT); |
| setTextColor(Color.WHITE); |
| setTypeface(Typeface.MONOSPACE); |
| setVisibility(View.INVISIBLE); |
| |
| final Resources res = getContext().getResources(); |
| |
| // get the default (will be updated later during measure) |
| mOutlineWidth = res.getDimensionPixelSize( |
| com.android.internal.R.dimen.subtitle_outline_width); |
| mShadowRadius = res.getDimensionPixelSize( |
| com.android.internal.R.dimen.subtitle_shadow_radius); |
| mShadowOffset = res.getDimensionPixelSize( |
| com.android.internal.R.dimen.subtitle_shadow_offset); |
| } |
| |
| void setCaptionStyle(CaptionStyle captionStyle) { |
| mTextColor = captionStyle.foregroundColor; |
| mBgColor = captionStyle.backgroundColor; |
| mEdgeType = captionStyle.edgeType; |
| mEdgeColor = captionStyle.edgeColor; |
| |
| setTextColor(mTextColor); |
| if (mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) { |
| setShadowLayer(mShadowRadius, mShadowOffset, mShadowOffset, mEdgeColor); |
| } else { |
| setShadowLayer(0, 0, 0, 0); |
| } |
| invalidate(); |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| float fontSize = MeasureSpec.getSize(heightMeasureSpec) * FONT_PADDING_RATIO; |
| setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize); |
| |
| mOutlineWidth = EDGE_OUTLINE_RATIO * fontSize + 1.0f; |
| mShadowRadius = EDGE_SHADOW_RATIO * fontSize + 1.0f;; |
| mShadowOffset = mShadowRadius; |
| |
| // set font scale in the X direction to match the required width |
| setScaleX(1.0f); |
| getPaint().getTextBounds(mDummyText, 0, mDummyText.length(), mTextBounds); |
| float actualTextWidth = mTextBounds.width(); |
| float requiredTextWidth = MeasureSpec.getSize(widthMeasureSpec); |
| setScaleX(requiredTextWidth / actualTextWidth); |
| |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| } |
| |
| @Override |
| protected void onDraw(Canvas c) { |
| if (mEdgeType == CaptionStyle.EDGE_TYPE_UNSPECIFIED |
| || mEdgeType == CaptionStyle.EDGE_TYPE_NONE |
| || mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) { |
| // these edge styles don't require a second pass |
| super.onDraw(c); |
| return; |
| } |
| |
| if (mEdgeType == CaptionStyle.EDGE_TYPE_OUTLINE) { |
| drawEdgeOutline(c); |
| } else { |
| // Raised or depressed |
| drawEdgeRaisedOrDepressed(c); |
| } |
| } |
| |
| private void drawEdgeOutline(Canvas c) { |
| TextPaint textPaint = getPaint(); |
| |
| Paint.Style previousStyle = textPaint.getStyle(); |
| Paint.Join previousJoin = textPaint.getStrokeJoin(); |
| float previousWidth = textPaint.getStrokeWidth(); |
| |
| setTextColor(mEdgeColor); |
| textPaint.setStyle(Paint.Style.FILL_AND_STROKE); |
| textPaint.setStrokeJoin(Paint.Join.ROUND); |
| textPaint.setStrokeWidth(mOutlineWidth); |
| |
| // Draw outline and background only. |
| super.onDraw(c); |
| |
| // Restore original settings. |
| setTextColor(mTextColor); |
| textPaint.setStyle(previousStyle); |
| textPaint.setStrokeJoin(previousJoin); |
| textPaint.setStrokeWidth(previousWidth); |
| |
| // Remove the background. |
| setBackgroundSpans(Color.TRANSPARENT); |
| // Draw foreground only. |
| super.onDraw(c); |
| // Restore the background. |
| setBackgroundSpans(mBgColor); |
| } |
| |
| private void drawEdgeRaisedOrDepressed(Canvas c) { |
| TextPaint textPaint = getPaint(); |
| |
| Paint.Style previousStyle = textPaint.getStyle(); |
| textPaint.setStyle(Paint.Style.FILL); |
| |
| final boolean raised = mEdgeType == CaptionStyle.EDGE_TYPE_RAISED; |
| final int colorUp = raised ? Color.WHITE : mEdgeColor; |
| final int colorDown = raised ? mEdgeColor : Color.WHITE; |
| final float offset = mShadowRadius / 2f; |
| |
| // Draw background and text with shadow up |
| setShadowLayer(mShadowRadius, -offset, -offset, colorUp); |
| super.onDraw(c); |
| |
| // Remove the background. |
| setBackgroundSpans(Color.TRANSPARENT); |
| |
| // Draw text with shadow down |
| setShadowLayer(mShadowRadius, +offset, +offset, colorDown); |
| super.onDraw(c); |
| |
| // Restore settings |
| textPaint.setStyle(previousStyle); |
| |
| // Restore the background. |
| setBackgroundSpans(mBgColor); |
| } |
| |
| private void setBackgroundSpans(int color) { |
| CharSequence text = getText(); |
| if (text instanceof Spannable) { |
| Spannable spannable = (Spannable) text; |
| Cea608CCParser.MutableBackgroundColorSpan[] bgSpans = spannable.getSpans( |
| 0, spannable.length(), Cea608CCParser.MutableBackgroundColorSpan.class); |
| for (int i = 0; i < bgSpans.length; i++) { |
| bgSpans[i].setBackgroundColor(color); |
| } |
| } |
| } |
| } |
| |
| private static class CCLayout extends LinearLayout implements ClosedCaptionLayout { |
| private static final int MAX_ROWS = Cea608CCParser.MAX_ROWS; |
| private static final float SAFE_AREA_RATIO = 0.9f; |
| |
| private final CCLineBox[] mLineBoxes = new CCLineBox[MAX_ROWS]; |
| |
| CCLayout(Context context) { |
| super(context); |
| setGravity(Gravity.START); |
| setOrientation(LinearLayout.VERTICAL); |
| for (int i = 0; i < MAX_ROWS; i++) { |
| mLineBoxes[i] = new CCLineBox(getContext()); |
| addView(mLineBoxes[i], LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); |
| } |
| } |
| |
| @Override |
| public void setCaptionStyle(CaptionStyle captionStyle) { |
| for (int i = 0; i < MAX_ROWS; i++) { |
| mLineBoxes[i].setCaptionStyle(captionStyle); |
| } |
| } |
| |
| @Override |
| public void setFontScale(float fontScale) { |
| // Ignores the font scale changes of the system wide CC preference. |
| } |
| |
| void update(SpannableStringBuilder[] textBuffer) { |
| for (int i = 0; i < MAX_ROWS; i++) { |
| if (textBuffer[i] != null) { |
| mLineBoxes[i].setText(textBuffer[i], TextView.BufferType.SPANNABLE); |
| mLineBoxes[i].setVisibility(View.VISIBLE); |
| } else { |
| mLineBoxes[i].setVisibility(View.INVISIBLE); |
| } |
| } |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| |
| int safeWidth = getMeasuredWidth(); |
| int safeHeight = getMeasuredHeight(); |
| |
| // CEA-608 assumes 4:3 video |
| if (safeWidth * 3 >= safeHeight * 4) { |
| safeWidth = safeHeight * 4 / 3; |
| } else { |
| safeHeight = safeWidth * 3 / 4; |
| } |
| safeWidth *= SAFE_AREA_RATIO; |
| safeHeight *= SAFE_AREA_RATIO; |
| |
| int lineHeight = safeHeight / MAX_ROWS; |
| int lineHeightMeasureSpec = MeasureSpec.makeMeasureSpec( |
| lineHeight, MeasureSpec.EXACTLY); |
| int lineWidthMeasureSpec = MeasureSpec.makeMeasureSpec( |
| safeWidth, MeasureSpec.EXACTLY); |
| |
| for (int i = 0; i < MAX_ROWS; i++) { |
| mLineBoxes[i].measure(lineWidthMeasureSpec, lineHeightMeasureSpec); |
| } |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int l, int t, int r, int b) { |
| // safe caption area |
| int viewPortWidth = r - l; |
| int viewPortHeight = b - t; |
| int safeWidth, safeHeight; |
| // CEA-608 assumes 4:3 video |
| if (viewPortWidth * 3 >= viewPortHeight * 4) { |
| safeWidth = viewPortHeight * 4 / 3; |
| safeHeight = viewPortHeight; |
| } else { |
| safeWidth = viewPortWidth; |
| safeHeight = viewPortWidth * 3 / 4; |
| } |
| safeWidth *= SAFE_AREA_RATIO; |
| safeHeight *= SAFE_AREA_RATIO; |
| int left = (viewPortWidth - safeWidth) / 2; |
| int top = (viewPortHeight - safeHeight) / 2; |
| |
| for (int i = 0; i < MAX_ROWS; i++) { |
| mLineBoxes[i].layout( |
| left, |
| top + safeHeight * i / MAX_ROWS, |
| left + safeWidth, |
| top + safeHeight * (i + 1) / MAX_ROWS); |
| } |
| } |
| } |
| } |