blob: 66759e53bea6fbd049e55c3a1207c00130e10d1e [file] [log] [blame]
/*
* Copyright (C) 2014 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 android.media;
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.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;
/** @hide */
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());
}
}
/** @hide */
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.
*
* @hide
*/
abstract class ClosedCaptionWidget extends ViewGroup implements SubtitleTrack.RenderingWidget {
/** @hide */
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);
}
}
}
}
/**
* @hide
*
* 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.
*
* @hide
*/
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.
*
* @hide
*/
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);
}
}
}
}