/* * Copyright (C) 2015 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.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; import android.os.Handler; import android.os.Message; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.CharacterStyle; import android.text.style.RelativeSizeSpan; import android.text.style.StyleSpan; import android.text.style.SubscriptSpan; import android.text.style.SuperscriptSpan; import android.text.style.UnderlineSpan; import android.util.AttributeSet; import android.text.Layout.Alignment; import android.util.Log; import android.text.TextUtils; 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.widget.RelativeLayout; import android.widget.TextView; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Vector; import com.android.internal.widget.SubtitleView; /** @hide */ public class Cea708CaptionRenderer extends SubtitleController.Renderer { private final Context mContext; private Cea708CCWidget mCCWidget; public Cea708CaptionRenderer(Context context) { mContext = context; } @Override public boolean supports(MediaFormat format) { if (format.containsKey(MediaFormat.KEY_MIME)) { String mimeType = format.getString(MediaFormat.KEY_MIME); return MediaPlayer.MEDIA_MIMETYPE_TEXT_CEA_708.equals(mimeType); } return false; } @Override public SubtitleTrack createTrack(MediaFormat format) { String mimeType = format.getString(MediaFormat.KEY_MIME); if (MediaPlayer.MEDIA_MIMETYPE_TEXT_CEA_708.equals(mimeType)) { if (mCCWidget == null) { mCCWidget = new Cea708CCWidget(mContext); } return new Cea708CaptionTrack(mCCWidget, format); } throw new RuntimeException("No matching format: " + format.toString()); } } /** @hide */ class Cea708CaptionTrack extends SubtitleTrack { private final Cea708CCParser mCCParser; private final Cea708CCWidget mRenderingWidget; Cea708CaptionTrack(Cea708CCWidget renderingWidget, MediaFormat format) { super(format); mRenderingWidget = renderingWidget; mCCParser = new Cea708CCParser(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 activeCues) { // Overriding with NO-OP, CC rendering by-passes this } } /** * @hide * * A class for parsing CEA-708, which is the standard for closed captioning for ATSC DTV. * *

ATSC DTV closed caption data are carried on picture user data of video streams. * This class starts to parse from picture user data payload, so extraction process of user_data * from video streams is up to outside of this code. * *

There are 4 steps to decode user_data to provide closed caption services. Step 1 and 2 are * done in NuPlayer and libstagefright. * *

Step 1. user_data -> CcPacket

* *

First, user_data consists of cc_data packets, which are 3-byte segments. Here, CcPacket is a * collection of cc_data packets in a frame along with same presentation timestamp. Because cc_data * packets must be reassembled in the frame display order, CcPackets are reordered. * *

Step 2. CcPacket -> DTVCC packet

* *

Each cc_data packet has a one byte for declaring a type of itself and data validity, and the * subsequent two bytes for input data of a DTVCC packet. There are 4 types for cc_data packet. * We're interested in DTVCC_PACKET_START(type 3) and DTVCC_PACKET_DATA(type 2). Each DTVCC packet * begins with DTVCC_PACKET_START(type 3) and the following cc_data packets which has * DTVCC_PACKET_DATA(type 2) are appended into the DTVCC packet being assembled. * *

Step 3. DTVCC packet -> Service Blocks

* *

A DTVCC packet consists of multiple service blocks. Each service block represents a caption * track and has a service number, which ranges from 1 to 63, that denotes caption track identity. * In here, we listen at most one chosen caption track by service number. Otherwise, just skip the * other service blocks. * *

Step 4. Interpreting Service Block Data ({@link #parseServiceBlockData}, {@code parseXX}, * and {@link #parseExt1} methods)

* *

Service block data is actual caption stream. it looks similar to telnet. It uses most parts of * ASCII table and consists of specially defined commands and some ASCII control codes which work * in a behavior slightly different from their original purpose. ASCII control codes and caption * commands are explicit instructions that control the state of a closed caption service and the * other ASCII and text codes are implicit instructions that send their characters to buffer. * *

There are 4 main code groups and 4 extended code groups. Both the range of code groups are the * same as the range of a byte. * *

4 main code groups: C0, C1, G0, G1 *
4 extended code groups: C2, C3, G2, G3 * *

Each code group has its own handle method. For example, {@link #parseC0} handles C0 code group * and so on. And {@link #parseServiceBlockData} method maps a stream on the main code groups while * {@link #parseExt1} method maps on the extended code groups. * *

The main code groups: *

*

Most of the extended code groups are being skipped. * */ class Cea708CCParser { private static final String TAG = "Cea708CCParser"; private static final boolean DEBUG = false; private static final String MUSIC_NOTE_CHAR = new String( "\u266B".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); private final StringBuffer mBuffer = new StringBuffer(); private int mCommand = 0; // Assign a dummy listener in order to avoid null checks. private DisplayListener mListener = new DisplayListener() { @Override public void emitEvent(CaptionEvent event) { // do nothing } }; /** * {@link Cea708Parser} emits caption event of three different types. * {@link DisplayListener#emitEvent} is invoked with the parameter * {@link CaptionEvent} to pass all the results to an observer of the decoding process . * *

{@link CaptionEvent#type} determines the type of the result and * {@link CaptionEvent#obj} contains the output value of a caption event. * The observer must do the casting to the corresponding type. * *

*/ public static final int CAPTION_EMIT_TYPE_BUFFER = 1; public static final int CAPTION_EMIT_TYPE_CONTROL = 2; public static final int CAPTION_EMIT_TYPE_COMMAND_CWX = 3; public static final int CAPTION_EMIT_TYPE_COMMAND_CLW = 4; public static final int CAPTION_EMIT_TYPE_COMMAND_DSW = 5; public static final int CAPTION_EMIT_TYPE_COMMAND_HDW = 6; public static final int CAPTION_EMIT_TYPE_COMMAND_TGW = 7; public static final int CAPTION_EMIT_TYPE_COMMAND_DLW = 8; public static final int CAPTION_EMIT_TYPE_COMMAND_DLY = 9; public static final int CAPTION_EMIT_TYPE_COMMAND_DLC = 10; public static final int CAPTION_EMIT_TYPE_COMMAND_RST = 11; public static final int CAPTION_EMIT_TYPE_COMMAND_SPA = 12; public static final int CAPTION_EMIT_TYPE_COMMAND_SPC = 13; public static final int CAPTION_EMIT_TYPE_COMMAND_SPL = 14; public static final int CAPTION_EMIT_TYPE_COMMAND_SWA = 15; public static final int CAPTION_EMIT_TYPE_COMMAND_DFX = 16; Cea708CCParser(DisplayListener listener) { if (listener != null) { mListener = listener; } } interface DisplayListener { void emitEvent(CaptionEvent event); } private void emitCaptionEvent(CaptionEvent captionEvent) { // Emit the existing string buffer before a new event is arrived. emitCaptionBuffer(); mListener.emitEvent(captionEvent); } private void emitCaptionBuffer() { if (mBuffer.length() > 0) { mListener.emitEvent(new CaptionEvent(CAPTION_EMIT_TYPE_BUFFER, mBuffer.toString())); mBuffer.setLength(0); } } // Step 3. DTVCC packet -> Service Blocks (parseDtvCcPacket method) public void parse(byte[] data) { // From this point, starts to read DTVCC coding layer. // First, identify code groups, which is defined in CEA-708B Section 7.1. int pos = 0; while (pos < data.length) { pos = parseServiceBlockData(data, pos); } // Emit the buffer after reading codes. emitCaptionBuffer(); } // Step 4. Main code groups private int parseServiceBlockData(byte[] data, int pos) { // For the details of the ranges of DTVCC code groups, see CEA-708B Table 6. mCommand = data[pos] & 0xff; ++pos; if (mCommand == Const.CODE_C0_EXT1) { if (DEBUG) { Log.d(TAG, String.format("parseServiceBlockData EXT1 %x", mCommand)); } pos = parseExt1(data, pos); } else if (mCommand >= Const.CODE_C0_RANGE_START && mCommand <= Const.CODE_C0_RANGE_END) { if (DEBUG) { Log.d(TAG, String.format("parseServiceBlockData C0 %x", mCommand)); } pos = parseC0(data, pos); } else if (mCommand >= Const.CODE_C1_RANGE_START && mCommand <= Const.CODE_C1_RANGE_END) { if (DEBUG) { Log.d(TAG, String.format("parseServiceBlockData C1 %x", mCommand)); } pos = parseC1(data, pos); } else if (mCommand >= Const.CODE_G0_RANGE_START && mCommand <= Const.CODE_G0_RANGE_END) { if (DEBUG) { Log.d(TAG, String.format("parseServiceBlockData G0 %x", mCommand)); } pos = parseG0(data, pos); } else if (mCommand >= Const.CODE_G1_RANGE_START && mCommand <= Const.CODE_G1_RANGE_END) { if (DEBUG) { Log.d(TAG, String.format("parseServiceBlockData G1 %x", mCommand)); } pos = parseG1(data, pos); } return pos; } private int parseC0(byte[] data, int pos) { // For the details of C0 code group, see CEA-708B Section 7.4.1. // CL Group: C0 Subset of ASCII Control codes if (mCommand >= Const.CODE_C0_SKIP2_RANGE_START && mCommand <= Const.CODE_C0_SKIP2_RANGE_END) { if (mCommand == Const.CODE_C0_P16) { // P16 escapes next two bytes for the large character maps.(no standard rule) // For Korea broadcasting, express whole letters by using this. try { if (data[pos] == 0) { mBuffer.append((char) data[pos + 1]); } else { String value = new String(Arrays.copyOfRange(data, pos, pos + 2), "EUC-KR"); mBuffer.append(value); } } catch (UnsupportedEncodingException e) { Log.e(TAG, "P16 Code - Could not find supported encoding", e); } } pos += 2; } else if (mCommand >= Const.CODE_C0_SKIP1_RANGE_START && mCommand <= Const.CODE_C0_SKIP1_RANGE_END) { ++pos; } else { // NUL, BS, FF, CR interpreted as they are in ASCII control codes. // HCR moves the pen location to th beginning of the current line and deletes contents. // FF clears the screen and moves the pen location to (0,0). // ETX is the NULL command which is used to flush text to the current window when no // other command is pending. switch (mCommand) { case Const.CODE_C0_NUL: break; case Const.CODE_C0_ETX: emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); break; case Const.CODE_C0_BS: emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); break; case Const.CODE_C0_FF: emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); break; case Const.CODE_C0_CR: mBuffer.append('\n'); break; case Const.CODE_C0_HCR: emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); break; default: break; } } return pos; } private int parseC1(byte[] data, int pos) { // For the details of C1 code group, see CEA-708B Section 8.10. // CR Group: C1 Caption Control Codes switch (mCommand) { case Const.CODE_C1_CW0: case Const.CODE_C1_CW1: case Const.CODE_C1_CW2: case Const.CODE_C1_CW3: case Const.CODE_C1_CW4: case Const.CODE_C1_CW5: case Const.CODE_C1_CW6: case Const.CODE_C1_CW7: { // SetCurrentWindow0-7 int windowId = mCommand - Const.CODE_C1_CW0; emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_CWX, windowId)); if (DEBUG) { Log.d(TAG, String.format("CaptionCommand CWX windowId: %d", windowId)); } break; } case Const.CODE_C1_CLW: { // ClearWindows int windowBitmap = data[pos] & 0xff; ++pos; emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_CLW, windowBitmap)); if (DEBUG) { Log.d(TAG, String.format("CaptionCommand CLW windowBitmap: %d", windowBitmap)); } break; } case Const.CODE_C1_DSW: { // DisplayWindows int windowBitmap = data[pos] & 0xff; ++pos; emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DSW, windowBitmap)); if (DEBUG) { Log.d(TAG, String.format("CaptionCommand DSW windowBitmap: %d", windowBitmap)); } break; } case Const.CODE_C1_HDW: { // HideWindows int windowBitmap = data[pos] & 0xff; ++pos; emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_HDW, windowBitmap)); if (DEBUG) { Log.d(TAG, String.format("CaptionCommand HDW windowBitmap: %d", windowBitmap)); } break; } case Const.CODE_C1_TGW: { // ToggleWindows int windowBitmap = data[pos] & 0xff; ++pos; emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_TGW, windowBitmap)); if (DEBUG) { Log.d(TAG, String.format("CaptionCommand TGW windowBitmap: %d", windowBitmap)); } break; } case Const.CODE_C1_DLW: { // DeleteWindows int windowBitmap = data[pos] & 0xff; ++pos; emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLW, windowBitmap)); if (DEBUG) { Log.d(TAG, String.format("CaptionCommand DLW windowBitmap: %d", windowBitmap)); } break; } case Const.CODE_C1_DLY: { // Delay int tenthsOfSeconds = data[pos] & 0xff; ++pos; emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLY, tenthsOfSeconds)); if (DEBUG) { Log.d(TAG, String.format("CaptionCommand DLY %d tenths of seconds", tenthsOfSeconds)); } break; } case Const.CODE_C1_DLC: { // DelayCancel emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLC, null)); if (DEBUG) { Log.d(TAG, "CaptionCommand DLC"); } break; } case Const.CODE_C1_RST: { // Reset emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_RST, null)); if (DEBUG) { Log.d(TAG, "CaptionCommand RST"); } break; } case Const.CODE_C1_SPA: { // SetPenAttributes int textTag = (data[pos] & 0xf0) >> 4; int penSize = data[pos] & 0x03; int penOffset = (data[pos] & 0x0c) >> 2; boolean italic = (data[pos + 1] & 0x80) != 0; boolean underline = (data[pos + 1] & 0x40) != 0; int edgeType = (data[pos + 1] & 0x38) >> 3; int fontTag = data[pos + 1] & 0x7; pos += 2; emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SPA, new CaptionPenAttr(penSize, penOffset, textTag, fontTag, edgeType, underline, italic))); if (DEBUG) { Log.d(TAG, String.format( "CaptionCommand SPA penSize: %d, penOffset: %d, textTag: %d, " + "fontTag: %d, edgeType: %d, underline: %s, italic: %s", penSize, penOffset, textTag, fontTag, edgeType, underline, italic)); } break; } case Const.CODE_C1_SPC: { // SetPenColor int opacity = (data[pos] & 0xc0) >> 6; int red = (data[pos] & 0x30) >> 4; int green = (data[pos] & 0x0c) >> 2; int blue = data[pos] & 0x03; CaptionColor foregroundColor = new CaptionColor(opacity, red, green, blue); ++pos; opacity = (data[pos] & 0xc0) >> 6; red = (data[pos] & 0x30) >> 4; green = (data[pos] & 0x0c) >> 2; blue = data[pos] & 0x03; CaptionColor backgroundColor = new CaptionColor(opacity, red, green, blue); ++pos; red = (data[pos] & 0x30) >> 4; green = (data[pos] & 0x0c) >> 2; blue = data[pos] & 0x03; CaptionColor edgeColor = new CaptionColor( CaptionColor.OPACITY_SOLID, red, green, blue); ++pos; emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SPC, new CaptionPenColor(foregroundColor, backgroundColor, edgeColor))); if (DEBUG) { Log.d(TAG, String.format( "CaptionCommand SPC foregroundColor %s backgroundColor %s edgeColor %s", foregroundColor, backgroundColor, edgeColor)); } break; } case Const.CODE_C1_SPL: { // SetPenLocation // column is normally 0-31 for 4:3 formats, and 0-41 for 16:9 formats int row = data[pos] & 0x0f; int column = data[pos + 1] & 0x3f; pos += 2; emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SPL, new CaptionPenLocation(row, column))); if (DEBUG) { Log.d(TAG, String.format("CaptionCommand SPL row: %d, column: %d", row, column)); } break; } case Const.CODE_C1_SWA: { // SetWindowAttributes int opacity = (data[pos] & 0xc0) >> 6; int red = (data[pos] & 0x30) >> 4; int green = (data[pos] & 0x0c) >> 2; int blue = data[pos] & 0x03; CaptionColor fillColor = new CaptionColor(opacity, red, green, blue); int borderType = (data[pos + 1] & 0xc0) >> 6 | (data[pos + 2] & 0x80) >> 5; red = (data[pos + 1] & 0x30) >> 4; green = (data[pos + 1] & 0x0c) >> 2; blue = data[pos + 1] & 0x03; CaptionColor borderColor = new CaptionColor( CaptionColor.OPACITY_SOLID, red, green, blue); boolean wordWrap = (data[pos + 2] & 0x40) != 0; int printDirection = (data[pos + 2] & 0x30) >> 4; int scrollDirection = (data[pos + 2] & 0x0c) >> 2; int justify = (data[pos + 2] & 0x03); int effectSpeed = (data[pos + 3] & 0xf0) >> 4; int effectDirection = (data[pos + 3] & 0x0c) >> 2; int displayEffect = data[pos + 3] & 0x3; pos += 4; emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SWA, new CaptionWindowAttr(fillColor, borderColor, borderType, wordWrap, printDirection, scrollDirection, justify, effectDirection, effectSpeed, displayEffect))); if (DEBUG) { Log.d(TAG, String.format( "CaptionCommand SWA fillColor: %s, borderColor: %s, borderType: %d" + "wordWrap: %s, printDirection: %d, scrollDirection: %d, " + "justify: %s, effectDirection: %d, effectSpeed: %d, " + "displayEffect: %d", fillColor, borderColor, borderType, wordWrap, printDirection, scrollDirection, justify, effectDirection, effectSpeed, displayEffect)); } break; } case Const.CODE_C1_DF0: case Const.CODE_C1_DF1: case Const.CODE_C1_DF2: case Const.CODE_C1_DF3: case Const.CODE_C1_DF4: case Const.CODE_C1_DF5: case Const.CODE_C1_DF6: case Const.CODE_C1_DF7: { // DefineWindow0-7 int windowId = mCommand - Const.CODE_C1_DF0; boolean visible = (data[pos] & 0x20) != 0; boolean rowLock = (data[pos] & 0x10) != 0; boolean columnLock = (data[pos] & 0x08) != 0; int priority = data[pos] & 0x07; boolean relativePositioning = (data[pos + 1] & 0x80) != 0; int anchorVertical = data[pos + 1] & 0x7f; int anchorHorizontal = data[pos + 2] & 0xff; int anchorId = (data[pos + 3] & 0xf0) >> 4; int rowCount = data[pos + 3] & 0x0f; int columnCount = data[pos + 4] & 0x3f; int windowStyle = (data[pos + 5] & 0x38) >> 3; int penStyle = data[pos + 5] & 0x07; pos += 6; emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DFX, new CaptionWindow(windowId, visible, rowLock, columnLock, priority, relativePositioning, anchorVertical, anchorHorizontal, anchorId, rowCount, columnCount, penStyle, windowStyle))); if (DEBUG) { Log.d(TAG, String.format( "CaptionCommand DFx windowId: %d, priority: %d, columnLock: %s, " + "rowLock: %s, visible: %s, anchorVertical: %d, " + "relativePositioning: %s, anchorHorizontal: %d, " + "rowCount: %d, anchorId: %d, columnCount: %d, penStyle: %d, " + "windowStyle: %d", windowId, priority, columnLock, rowLock, visible, anchorVertical, relativePositioning, anchorHorizontal, rowCount, anchorId, columnCount, penStyle, windowStyle)); } break; } default: break; } return pos; } private int parseG0(byte[] data, int pos) { // For the details of G0 code group, see CEA-708B Section 7.4.3. // GL Group: G0 Modified version of ANSI X3.4 Printable Character Set (ASCII) if (mCommand == Const.CODE_G0_MUSICNOTE) { // Music note. mBuffer.append(MUSIC_NOTE_CHAR); } else { // Put ASCII code into buffer. mBuffer.append((char) mCommand); } return pos; } private int parseG1(byte[] data, int pos) { // For the details of G0 code group, see CEA-708B Section 7.4.4. // GR Group: G1 ISO 8859-1 Latin 1 Characters // Put ASCII Extended character set into buffer. mBuffer.append((char) mCommand); return pos; } // Step 4. Extended code groups private int parseExt1(byte[] data, int pos) { // For the details of EXT1 code group, see CEA-708B Section 7.2. mCommand = data[pos] & 0xff; ++pos; if (mCommand >= Const.CODE_C2_RANGE_START && mCommand <= Const.CODE_C2_RANGE_END) { pos = parseC2(data, pos); } else if (mCommand >= Const.CODE_C3_RANGE_START && mCommand <= Const.CODE_C3_RANGE_END) { pos = parseC3(data, pos); } else if (mCommand >= Const.CODE_G2_RANGE_START && mCommand <= Const.CODE_G2_RANGE_END) { pos = parseG2(data, pos); } else if (mCommand >= Const.CODE_G3_RANGE_START && mCommand <= Const.CODE_G3_RANGE_END) { pos = parseG3(data ,pos); } return pos; } private int parseC2(byte[] data, int pos) { // For the details of C2 code group, see CEA-708B Section 7.4.7. // Extended Miscellaneous Control Codes // C2 Table : No commands as of CEA-708B. A decoder must skip. if (mCommand >= Const.CODE_C2_SKIP0_RANGE_START && mCommand <= Const.CODE_C2_SKIP0_RANGE_END) { // Do nothing. } else if (mCommand >= Const.CODE_C2_SKIP1_RANGE_START && mCommand <= Const.CODE_C2_SKIP1_RANGE_END) { ++pos; } else if (mCommand >= Const.CODE_C2_SKIP2_RANGE_START && mCommand <= Const.CODE_C2_SKIP2_RANGE_END) { pos += 2; } else if (mCommand >= Const.CODE_C2_SKIP3_RANGE_START && mCommand <= Const.CODE_C2_SKIP3_RANGE_END) { pos += 3; } return pos; } private int parseC3(byte[] data, int pos) { // For the details of C3 code group, see CEA-708B Section 7.4.8. // Extended Control Code Set 2 // C3 Table : No commands as of CEA-708B. A decoder must skip. if (mCommand >= Const.CODE_C3_SKIP4_RANGE_START && mCommand <= Const.CODE_C3_SKIP4_RANGE_END) { pos += 4; } else if (mCommand >= Const.CODE_C3_SKIP5_RANGE_START && mCommand <= Const.CODE_C3_SKIP5_RANGE_END) { pos += 5; } return pos; } private int parseG2(byte[] data, int pos) { // For the details of C3 code group, see CEA-708B Section 7.4.5. // Extended Control Code Set 1(G2 Table) switch (mCommand) { case Const.CODE_G2_TSP: // TODO : TSP is the Transparent space break; case Const.CODE_G2_NBTSP: // TODO : NBTSP is Non-Breaking Transparent Space. break; case Const.CODE_G2_BLK: // TODO : BLK indicates a solid block which fills the entire character block // TODO : with a solid foreground color. break; default: break; } return pos; } private int parseG3(byte[] data, int pos) { // For the details of C3 code group, see CEA-708B Section 7.4.6. // Future characters and icons(G3 Table) if (mCommand == Const.CODE_G3_CC) { // TODO : [CC] icon with square corners } // Do nothing return pos; } /** * @hide * * Collection of CEA-708 structures. */ private static class Const { private Const() { } // For the details of the ranges of DTVCC code groups, see CEA-708B Table 6. public static final int CODE_C0_RANGE_START = 0x00; public static final int CODE_C0_RANGE_END = 0x1f; public static final int CODE_C1_RANGE_START = 0x80; public static final int CODE_C1_RANGE_END = 0x9f; public static final int CODE_G0_RANGE_START = 0x20; public static final int CODE_G0_RANGE_END = 0x7f; public static final int CODE_G1_RANGE_START = 0xa0; public static final int CODE_G1_RANGE_END = 0xff; public static final int CODE_C2_RANGE_START = 0x00; public static final int CODE_C2_RANGE_END = 0x1f; public static final int CODE_C3_RANGE_START = 0x80; public static final int CODE_C3_RANGE_END = 0x9f; public static final int CODE_G2_RANGE_START = 0x20; public static final int CODE_G2_RANGE_END = 0x7f; public static final int CODE_G3_RANGE_START = 0xa0; public static final int CODE_G3_RANGE_END = 0xff; // The following ranges are defined in CEA-708B Section 7.4.1. public static final int CODE_C0_SKIP2_RANGE_START = 0x18; public static final int CODE_C0_SKIP2_RANGE_END = 0x1f; public static final int CODE_C0_SKIP1_RANGE_START = 0x10; public static final int CODE_C0_SKIP1_RANGE_END = 0x17; // The following ranges are defined in CEA-708B Section 7.4.7. public static final int CODE_C2_SKIP0_RANGE_START = 0x00; public static final int CODE_C2_SKIP0_RANGE_END = 0x07; public static final int CODE_C2_SKIP1_RANGE_START = 0x08; public static final int CODE_C2_SKIP1_RANGE_END = 0x0f; public static final int CODE_C2_SKIP2_RANGE_START = 0x10; public static final int CODE_C2_SKIP2_RANGE_END = 0x17; public static final int CODE_C2_SKIP3_RANGE_START = 0x18; public static final int CODE_C2_SKIP3_RANGE_END = 0x1f; // The following ranges are defined in CEA-708B Section 7.4.8. public static final int CODE_C3_SKIP4_RANGE_START = 0x80; public static final int CODE_C3_SKIP4_RANGE_END = 0x87; public static final int CODE_C3_SKIP5_RANGE_START = 0x88; public static final int CODE_C3_SKIP5_RANGE_END = 0x8f; // The following values are the special characters of CEA-708 spec. public static final int CODE_C0_NUL = 0x00; public static final int CODE_C0_ETX = 0x03; public static final int CODE_C0_BS = 0x08; public static final int CODE_C0_FF = 0x0c; public static final int CODE_C0_CR = 0x0d; public static final int CODE_C0_HCR = 0x0e; public static final int CODE_C0_EXT1 = 0x10; public static final int CODE_C0_P16 = 0x18; public static final int CODE_G0_MUSICNOTE = 0x7f; public static final int CODE_G2_TSP = 0x20; public static final int CODE_G2_NBTSP = 0x21; public static final int CODE_G2_BLK = 0x30; public static final int CODE_G3_CC = 0xa0; // The following values are the command bits of CEA-708 spec. public static final int CODE_C1_CW0 = 0x80; public static final int CODE_C1_CW1 = 0x81; public static final int CODE_C1_CW2 = 0x82; public static final int CODE_C1_CW3 = 0x83; public static final int CODE_C1_CW4 = 0x84; public static final int CODE_C1_CW5 = 0x85; public static final int CODE_C1_CW6 = 0x86; public static final int CODE_C1_CW7 = 0x87; public static final int CODE_C1_CLW = 0x88; public static final int CODE_C1_DSW = 0x89; public static final int CODE_C1_HDW = 0x8a; public static final int CODE_C1_TGW = 0x8b; public static final int CODE_C1_DLW = 0x8c; public static final int CODE_C1_DLY = 0x8d; public static final int CODE_C1_DLC = 0x8e; public static final int CODE_C1_RST = 0x8f; public static final int CODE_C1_SPA = 0x90; public static final int CODE_C1_SPC = 0x91; public static final int CODE_C1_SPL = 0x92; public static final int CODE_C1_SWA = 0x97; public static final int CODE_C1_DF0 = 0x98; public static final int CODE_C1_DF1 = 0x99; public static final int CODE_C1_DF2 = 0x9a; public static final int CODE_C1_DF3 = 0x9b; public static final int CODE_C1_DF4 = 0x9c; public static final int CODE_C1_DF5 = 0x9d; public static final int CODE_C1_DF6 = 0x9e; public static final int CODE_C1_DF7 = 0x9f; } /** * @hide * * CEA-708B-specific color. */ public static class CaptionColor { public static final int OPACITY_SOLID = 0; public static final int OPACITY_FLASH = 1; public static final int OPACITY_TRANSLUCENT = 2; public static final int OPACITY_TRANSPARENT = 3; private static final int[] COLOR_MAP = new int[] { 0x00, 0x0f, 0xf0, 0xff }; private static final int[] OPACITY_MAP = new int[] { 0xff, 0xfe, 0x80, 0x00 }; public final int opacity; public final int red; public final int green; public final int blue; public CaptionColor(int opacity, int red, int green, int blue) { this.opacity = opacity; this.red = red; this.green = green; this.blue = blue; } public int getArgbValue() { return Color.argb( OPACITY_MAP[opacity], COLOR_MAP[red], COLOR_MAP[green], COLOR_MAP[blue]); } } /** * @hide * * Caption event generated by {@link Cea708CCParser}. */ public static class CaptionEvent { public final int type; public final Object obj; public CaptionEvent(int type, Object obj) { this.type = type; this.obj = obj; } } /** * @hide * * Pen style information. */ public static class CaptionPenAttr { // Pen sizes public static final int PEN_SIZE_SMALL = 0; public static final int PEN_SIZE_STANDARD = 1; public static final int PEN_SIZE_LARGE = 2; // Offsets public static final int OFFSET_SUBSCRIPT = 0; public static final int OFFSET_NORMAL = 1; public static final int OFFSET_SUPERSCRIPT = 2; public final int penSize; public final int penOffset; public final int textTag; public final int fontTag; public final int edgeType; public final boolean underline; public final boolean italic; public CaptionPenAttr(int penSize, int penOffset, int textTag, int fontTag, int edgeType, boolean underline, boolean italic) { this.penSize = penSize; this.penOffset = penOffset; this.textTag = textTag; this.fontTag = fontTag; this.edgeType = edgeType; this.underline = underline; this.italic = italic; } } /** * @hide * * {@link CaptionColor} objects that indicate the foreground, background, and edge color of a * pen. */ public static class CaptionPenColor { public final CaptionColor foregroundColor; public final CaptionColor backgroundColor; public final CaptionColor edgeColor; public CaptionPenColor(CaptionColor foregroundColor, CaptionColor backgroundColor, CaptionColor edgeColor) { this.foregroundColor = foregroundColor; this.backgroundColor = backgroundColor; this.edgeColor = edgeColor; } } /** * @hide * * Location information of a pen. */ public static class CaptionPenLocation { public final int row; public final int column; public CaptionPenLocation(int row, int column) { this.row = row; this.column = column; } } /** * @hide * * Attributes of a caption window, which is defined in CEA-708B. */ public static class CaptionWindowAttr { public final CaptionColor fillColor; public final CaptionColor borderColor; public final int borderType; public final boolean wordWrap; public final int printDirection; public final int scrollDirection; public final int justify; public final int effectDirection; public final int effectSpeed; public final int displayEffect; public CaptionWindowAttr(CaptionColor fillColor, CaptionColor borderColor, int borderType, boolean wordWrap, int printDirection, int scrollDirection, int justify, int effectDirection, int effectSpeed, int displayEffect) { this.fillColor = fillColor; this.borderColor = borderColor; this.borderType = borderType; this.wordWrap = wordWrap; this.printDirection = printDirection; this.scrollDirection = scrollDirection; this.justify = justify; this.effectDirection = effectDirection; this.effectSpeed = effectSpeed; this.displayEffect = displayEffect; } } /** * @hide * * Construction information of the caption window of CEA-708B. */ public static class CaptionWindow { public final int id; public final boolean visible; public final boolean rowLock; public final boolean columnLock; public final int priority; public final boolean relativePositioning; public final int anchorVertical; public final int anchorHorizontal; public final int anchorId; public final int rowCount; public final int columnCount; public final int penStyle; public final int windowStyle; public CaptionWindow(int id, boolean visible, boolean rowLock, boolean columnLock, int priority, boolean relativePositioning, int anchorVertical, int anchorHorizontal, int anchorId, int rowCount, int columnCount, int penStyle, int windowStyle) { this.id = id; this.visible = visible; this.rowLock = rowLock; this.columnLock = columnLock; this.priority = priority; this.relativePositioning = relativePositioning; this.anchorVertical = anchorVertical; this.anchorHorizontal = anchorHorizontal; this.anchorId = anchorId; this.rowCount = rowCount; this.columnCount = columnCount; this.penStyle = penStyle; this.windowStyle = windowStyle; } } } /** * Widget capable of rendering CEA-708 closed captions. * * @hide */ class Cea708CCWidget extends ClosedCaptionWidget implements Cea708CCParser.DisplayListener { private final CCHandler mCCHandler; public Cea708CCWidget(Context context) { this(context, null); } public Cea708CCWidget(Context context, AttributeSet attrs) { this(context, attrs, 0); } public Cea708CCWidget(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public Cea708CCWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mCCHandler = new CCHandler((CCLayout) mClosedCaptionLayout); } @Override public ClosedCaptionLayout createCaptionLayout(Context context) { return new CCLayout(context); } @Override public void emitEvent(Cea708CCParser.CaptionEvent event) { mCCHandler.processCaptionEvent(event); setSize(getWidth(), getHeight()); if (mListener != null) { mListener.onChanged(this); } } @Override public void onDraw(Canvas canvas) { super.onDraw(canvas); ((ViewGroup) mClosedCaptionLayout).draw(canvas); } /** * @hide * * A layout that scales its children using the given percentage value. */ static class ScaledLayout extends ViewGroup { private static final String TAG = "ScaledLayout"; private static final boolean DEBUG = false; private static final Comparator mRectTopLeftSorter = new Comparator() { @Override public int compare(Rect lhs, Rect rhs) { if (lhs.top != rhs.top) { return lhs.top - rhs.top; } else { return lhs.left - rhs.left; } } }; private Rect[] mRectArray; public ScaledLayout(Context context) { super(context); } /** * @hide * * ScaledLayoutParams stores the four scale factors. *
* Vertical coordinate system: (scaleStartRow * 100) % ~ (scaleEndRow * 100) % * Horizontal coordinate system: (scaleStartCol * 100) % ~ (scaleEndCol * 100) % *
* In XML, for example, *
         * {@code
         * 
         * }
         * 
*/ static class ScaledLayoutParams extends ViewGroup.LayoutParams { public static final float SCALE_UNSPECIFIED = -1; public float scaleStartRow; public float scaleEndRow; public float scaleStartCol; public float scaleEndCol; public ScaledLayoutParams(float scaleStartRow, float scaleEndRow, float scaleStartCol, float scaleEndCol) { super(MATCH_PARENT, MATCH_PARENT); this.scaleStartRow = scaleStartRow; this.scaleEndRow = scaleEndRow; this.scaleStartCol = scaleStartCol; this.scaleEndCol = scaleEndCol; } public ScaledLayoutParams(Context context, AttributeSet attrs) { super(MATCH_PARENT, MATCH_PARENT); } } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new ScaledLayoutParams(getContext(), attrs); } @Override protected boolean checkLayoutParams(LayoutParams p) { return (p instanceof ScaledLayoutParams); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); int width = widthSpecSize - getPaddingLeft() - getPaddingRight(); int height = heightSpecSize - getPaddingTop() - getPaddingBottom(); if (DEBUG) { Log.d(TAG, String.format("onMeasure width: %d, height: %d", width, height)); } int count = getChildCount(); mRectArray = new Rect[count]; for (int i = 0; i < count; ++i) { View child = getChildAt(i); ViewGroup.LayoutParams params = child.getLayoutParams(); float scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol; if (!(params instanceof ScaledLayoutParams)) { throw new RuntimeException( "A child of ScaledLayout cannot have the UNSPECIFIED scale factors"); } scaleStartRow = ((ScaledLayoutParams) params).scaleStartRow; scaleEndRow = ((ScaledLayoutParams) params).scaleEndRow; scaleStartCol = ((ScaledLayoutParams) params).scaleStartCol; scaleEndCol = ((ScaledLayoutParams) params).scaleEndCol; if (scaleStartRow < 0 || scaleStartRow > 1) { throw new RuntimeException("A child of ScaledLayout should have a range of " + "scaleStartRow between 0 and 1"); } if (scaleEndRow < scaleStartRow || scaleStartRow > 1) { throw new RuntimeException("A child of ScaledLayout should have a range of " + "scaleEndRow between scaleStartRow and 1"); } if (scaleEndCol < 0 || scaleEndCol > 1) { throw new RuntimeException("A child of ScaledLayout should have a range of " + "scaleStartCol between 0 and 1"); } if (scaleEndCol < scaleStartCol || scaleEndCol > 1) { throw new RuntimeException("A child of ScaledLayout should have a range of " + "scaleEndCol between scaleStartCol and 1"); } if (DEBUG) { Log.d(TAG, String.format("onMeasure child scaleStartRow: %f scaleEndRow: %f " + "scaleStartCol: %f scaleEndCol: %f", scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol)); } mRectArray[i] = new Rect((int) (scaleStartCol * width), (int) (scaleStartRow * height), (int) (scaleEndCol * width), (int) (scaleEndRow * height)); int childWidthSpec = MeasureSpec.makeMeasureSpec( (int) (width * (scaleEndCol - scaleStartCol)), MeasureSpec.EXACTLY); int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); child.measure(childWidthSpec, childHeightSpec); // If the height of the measured child view is bigger than the height of the // calculated region by the given ScaleLayoutParams, the height of the region should // be increased to fit the size of the child view. if (child.getMeasuredHeight() > mRectArray[i].height()) { int overflowedHeight = child.getMeasuredHeight() - mRectArray[i].height(); overflowedHeight = (overflowedHeight + 1) / 2; mRectArray[i].bottom += overflowedHeight; mRectArray[i].top -= overflowedHeight; if (mRectArray[i].top < 0) { mRectArray[i].bottom -= mRectArray[i].top; mRectArray[i].top = 0; } if (mRectArray[i].bottom > height) { mRectArray[i].top -= mRectArray[i].bottom - height; mRectArray[i].bottom = height; } } childHeightSpec = MeasureSpec.makeMeasureSpec( (int) (height * (scaleEndRow - scaleStartRow)), MeasureSpec.EXACTLY); child.measure(childWidthSpec, childHeightSpec); } // Avoid overlapping rectangles. // Step 1. Sort rectangles by position (top-left). int visibleRectCount = 0; int[] visibleRectGroup = new int[count]; Rect[] visibleRectArray = new Rect[count]; for (int i = 0; i < count; ++i) { if (getChildAt(i).getVisibility() == View.VISIBLE) { visibleRectGroup[visibleRectCount] = visibleRectCount; visibleRectArray[visibleRectCount] = mRectArray[i]; ++visibleRectCount; } } Arrays.sort(visibleRectArray, 0, visibleRectCount, mRectTopLeftSorter); // Step 2. Move down if there are overlapping rectangles. for (int i = 0; i < visibleRectCount - 1; ++i) { for (int j = i + 1; j < visibleRectCount; ++j) { if (Rect.intersects(visibleRectArray[i], visibleRectArray[j])) { visibleRectGroup[j] = visibleRectGroup[i]; visibleRectArray[j].set(visibleRectArray[j].left, visibleRectArray[i].bottom, visibleRectArray[j].right, visibleRectArray[i].bottom + visibleRectArray[j].height()); } } } // Step 3. Move up if there is any overflowed rectangle. for (int i = visibleRectCount - 1; i >= 0; --i) { if (visibleRectArray[i].bottom > height) { int overflowedHeight = visibleRectArray[i].bottom - height; for (int j = 0; j <= i; ++j) { if (visibleRectGroup[i] == visibleRectGroup[j]) { visibleRectArray[j].set(visibleRectArray[j].left, visibleRectArray[j].top - overflowedHeight, visibleRectArray[j].right, visibleRectArray[j].bottom - overflowedHeight); } } } } setMeasuredDimension(widthSpecSize, heightSpecSize); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int paddingLeft = getPaddingLeft(); int paddingTop = getPaddingTop(); int count = getChildCount(); for (int i = 0; i < count; ++i) { View child = getChildAt(i); if (child.getVisibility() != GONE) { int childLeft = paddingLeft + mRectArray[i].left; int childTop = paddingTop + mRectArray[i].top; int childBottom = paddingLeft + mRectArray[i].bottom; int childRight = paddingTop + mRectArray[i].right; if (DEBUG) { Log.d(TAG, String.format( "child layout bottom: %d left: %d right: %d top: %d", childBottom, childLeft, childRight, childTop)); } child.layout(childLeft, childTop, childRight, childBottom); } } } @Override public void dispatchDraw(Canvas canvas) { int paddingLeft = getPaddingLeft(); int paddingTop = getPaddingTop(); int count = getChildCount(); for (int i = 0; i < count; ++i) { View child = getChildAt(i); if (child.getVisibility() != GONE) { if (i >= mRectArray.length) { break; } int childLeft = paddingLeft + mRectArray[i].left; int childTop = paddingTop + mRectArray[i].top; final int saveCount = canvas.save(); canvas.translate(childLeft, childTop); child.draw(canvas); canvas.restoreToCount(saveCount); } } } } /** * @hide * * Layout containing the safe title area that helps the closed captions look more prominent. * *

This is required by CEA-708B. */ static class CCLayout extends ScaledLayout implements ClosedCaptionLayout { private static final float SAFE_TITLE_AREA_SCALE_START_X = 0.1f; private static final float SAFE_TITLE_AREA_SCALE_END_X = 0.9f; private static final float SAFE_TITLE_AREA_SCALE_START_Y = 0.1f; private static final float SAFE_TITLE_AREA_SCALE_END_Y = 0.9f; private final ScaledLayout mSafeTitleAreaLayout; public CCLayout(Context context) { super(context); mSafeTitleAreaLayout = new ScaledLayout(context); addView(mSafeTitleAreaLayout, new ScaledLayout.ScaledLayoutParams( SAFE_TITLE_AREA_SCALE_START_X, SAFE_TITLE_AREA_SCALE_END_X, SAFE_TITLE_AREA_SCALE_START_Y, SAFE_TITLE_AREA_SCALE_END_Y)); } public void addOrUpdateViewToSafeTitleArea(CCWindowLayout captionWindowLayout, ScaledLayoutParams scaledLayoutParams) { int index = mSafeTitleAreaLayout.indexOfChild(captionWindowLayout); if (index < 0) { mSafeTitleAreaLayout.addView(captionWindowLayout, scaledLayoutParams); return; } mSafeTitleAreaLayout.updateViewLayout(captionWindowLayout, scaledLayoutParams); } public void removeViewFromSafeTitleArea(CCWindowLayout captionWindowLayout) { mSafeTitleAreaLayout.removeView(captionWindowLayout); } public void setCaptionStyle(CaptionStyle style) { final int count = mSafeTitleAreaLayout.getChildCount(); for (int i = 0; i < count; ++i) { final CCWindowLayout windowLayout = (CCWindowLayout) mSafeTitleAreaLayout.getChildAt(i); windowLayout.setCaptionStyle(style); } } public void setFontScale(float fontScale) { final int count = mSafeTitleAreaLayout.getChildCount(); for (int i = 0; i < count; ++i) { final CCWindowLayout windowLayout = (CCWindowLayout) mSafeTitleAreaLayout.getChildAt(i); windowLayout.setFontScale(fontScale); } } } /** * @hide * * Renders the selected CC track. */ static class CCHandler implements Handler.Callback { // TODO: Remaining works // CaptionTrackRenderer does not support the full spec of CEA-708. The remaining works are // described in the follows. // C0 Table: Backspace, FF, and HCR are not supported. The rule for P16 is not standardized // but it is handled as EUC-KR charset for Korea broadcasting. // C1 Table: All the styles of windows and pens except underline, italic, pen size, and pen // offset specified in CEA-708 are ignored and this follows system wide CC // preferences for look and feel. SetPenLocation is not implemented. // G2 Table: TSP, NBTSP and BLK are not supported. // Text/commands: Word wrapping, fonts, row and column locking are not supported. private static final String TAG = "CCHandler"; private static final boolean DEBUG = false; private static final int TENTHS_OF_SECOND_IN_MILLIS = 100; // According to CEA-708B, there can exist up to 8 caption windows. private static final int CAPTION_WINDOWS_MAX = 8; private static final int CAPTION_ALL_WINDOWS_BITMAP = 255; private static final int MSG_DELAY_CANCEL = 1; private static final int MSG_CAPTION_CLEAR = 2; private static final long CAPTION_CLEAR_INTERVAL_MS = 60000; private final CCLayout mCCLayout; private boolean mIsDelayed = false; private CCWindowLayout mCurrentWindowLayout; private final CCWindowLayout[] mCaptionWindowLayouts = new CCWindowLayout[CAPTION_WINDOWS_MAX]; private final ArrayList mPendingCaptionEvents = new ArrayList<>(); private final Handler mHandler; public CCHandler(CCLayout ccLayout) { mCCLayout = ccLayout; mHandler = new Handler(this); } @Override public boolean handleMessage(Message msg) { switch (msg.what) { case MSG_DELAY_CANCEL: delayCancel(); return true; case MSG_CAPTION_CLEAR: clearWindows(CAPTION_ALL_WINDOWS_BITMAP); return true; } return false; } public void processCaptionEvent(Cea708CCParser.CaptionEvent event) { if (mIsDelayed) { mPendingCaptionEvents.add(event); return; } switch (event.type) { case Cea708CCParser.CAPTION_EMIT_TYPE_BUFFER: sendBufferToCurrentWindow((String) event.obj); break; case Cea708CCParser.CAPTION_EMIT_TYPE_CONTROL: sendControlToCurrentWindow((char) event.obj); break; case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_CWX: setCurrentWindowLayout((int) event.obj); break; case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_CLW: clearWindows((int) event.obj); break; case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DSW: displayWindows((int) event.obj); break; case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_HDW: hideWindows((int) event.obj); break; case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_TGW: toggleWindows((int) event.obj); break; case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DLW: deleteWindows((int) event.obj); break; case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DLY: delay((int) event.obj); break; case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DLC: delayCancel(); break; case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_RST: reset(); break; case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_SPA: setPenAttr((Cea708CCParser.CaptionPenAttr) event.obj); break; case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_SPC: setPenColor((Cea708CCParser.CaptionPenColor) event.obj); break; case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_SPL: setPenLocation((Cea708CCParser.CaptionPenLocation) event.obj); break; case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_SWA: setWindowAttr((Cea708CCParser.CaptionWindowAttr) event.obj); break; case Cea708CCParser.CAPTION_EMIT_TYPE_COMMAND_DFX: defineWindow((Cea708CCParser.CaptionWindow) event.obj); break; } } // The window related caption commands private void setCurrentWindowLayout(int windowId) { if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) { return; } CCWindowLayout windowLayout = mCaptionWindowLayouts[windowId]; if (windowLayout == null) { return; } if (DEBUG) { Log.d(TAG, "setCurrentWindowLayout to " + windowId); } mCurrentWindowLayout = windowLayout; } // Each bit of windowBitmap indicates a window. // If a bit is set, the window id is the same as the number of the trailing zeros of the // bit. private ArrayList getWindowsFromBitmap(int windowBitmap) { ArrayList windows = new ArrayList<>(); for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) { if ((windowBitmap & (1 << i)) != 0) { CCWindowLayout windowLayout = mCaptionWindowLayouts[i]; if (windowLayout != null) { windows.add(windowLayout); } } } return windows; } private void clearWindows(int windowBitmap) { if (windowBitmap == 0) { return; } for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { windowLayout.clear(); } } private void displayWindows(int windowBitmap) { if (windowBitmap == 0) { return; } for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { windowLayout.show(); } } private void hideWindows(int windowBitmap) { if (windowBitmap == 0) { return; } for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { windowLayout.hide(); } } private void toggleWindows(int windowBitmap) { if (windowBitmap == 0) { return; } for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { if (windowLayout.isShown()) { windowLayout.hide(); } else { windowLayout.show(); } } } private void deleteWindows(int windowBitmap) { if (windowBitmap == 0) { return; } for (CCWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { windowLayout.removeFromCaptionView(); mCaptionWindowLayouts[windowLayout.getCaptionWindowId()] = null; } } public void reset() { mCurrentWindowLayout = null; mIsDelayed = false; mPendingCaptionEvents.clear(); for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) { if (mCaptionWindowLayouts[i] != null) { mCaptionWindowLayouts[i].removeFromCaptionView(); } mCaptionWindowLayouts[i] = null; } mCCLayout.setVisibility(View.INVISIBLE); mHandler.removeMessages(MSG_CAPTION_CLEAR); } private void setWindowAttr(Cea708CCParser.CaptionWindowAttr windowAttr) { if (mCurrentWindowLayout != null) { mCurrentWindowLayout.setWindowAttr(windowAttr); } } private void defineWindow(Cea708CCParser.CaptionWindow window) { if (window == null) { return; } int windowId = window.id; if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) { return; } CCWindowLayout windowLayout = mCaptionWindowLayouts[windowId]; if (windowLayout == null) { windowLayout = new CCWindowLayout(mCCLayout.getContext()); } windowLayout.initWindow(mCCLayout, window); mCurrentWindowLayout = mCaptionWindowLayouts[windowId] = windowLayout; } // The job related caption commands private void delay(int tenthsOfSeconds) { if (tenthsOfSeconds < 0 || tenthsOfSeconds > 255) { return; } mIsDelayed = true; mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_DELAY_CANCEL), tenthsOfSeconds * TENTHS_OF_SECOND_IN_MILLIS); } private void delayCancel() { mIsDelayed = false; processPendingBuffer(); } private void processPendingBuffer() { for (Cea708CCParser.CaptionEvent event : mPendingCaptionEvents) { processCaptionEvent(event); } mPendingCaptionEvents.clear(); } // The implicit write caption commands private void sendControlToCurrentWindow(char control) { if (mCurrentWindowLayout != null) { mCurrentWindowLayout.sendControl(control); } } private void sendBufferToCurrentWindow(String buffer) { if (mCurrentWindowLayout != null) { mCurrentWindowLayout.sendBuffer(buffer); mHandler.removeMessages(MSG_CAPTION_CLEAR); mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CAPTION_CLEAR), CAPTION_CLEAR_INTERVAL_MS); } } // The pen related caption commands private void setPenAttr(Cea708CCParser.CaptionPenAttr attr) { if (mCurrentWindowLayout != null) { mCurrentWindowLayout.setPenAttr(attr); } } private void setPenColor(Cea708CCParser.CaptionPenColor color) { if (mCurrentWindowLayout != null) { mCurrentWindowLayout.setPenColor(color); } } private void setPenLocation(Cea708CCParser.CaptionPenLocation location) { if (mCurrentWindowLayout != null) { mCurrentWindowLayout.setPenLocation(location.row, location.column); } } } /** * @hide * * Layout which renders a caption window of CEA-708B. It contains a {@link TextView} that takes * care of displaying the actual CC text. */ static class CCWindowLayout extends RelativeLayout implements View.OnLayoutChangeListener { private static final String TAG = "CCWindowLayout"; private static final float PROPORTION_PEN_SIZE_SMALL = .75f; private static final float PROPORTION_PEN_SIZE_LARGE = 1.25f; // The following values indicates the maximum cell number of a window. private static final int ANCHOR_RELATIVE_POSITIONING_MAX = 99; private static final int ANCHOR_VERTICAL_MAX = 74; private static final int ANCHOR_HORIZONTAL_16_9_MAX = 209; private static final int MAX_COLUMN_COUNT_16_9 = 42; // The following values indicates a gravity of a window. private static final int ANCHOR_MODE_DIVIDER = 3; private static final int ANCHOR_HORIZONTAL_MODE_LEFT = 0; private static final int ANCHOR_HORIZONTAL_MODE_CENTER = 1; private static final int ANCHOR_HORIZONTAL_MODE_RIGHT = 2; private static final int ANCHOR_VERTICAL_MODE_TOP = 0; private static final int ANCHOR_VERTICAL_MODE_CENTER = 1; private static final int ANCHOR_VERTICAL_MODE_BOTTOM = 2; private CCLayout mCCLayout; private CCView mCCView; private CaptionStyle mCaptionStyle; private int mRowLimit = 0; private final SpannableStringBuilder mBuilder = new SpannableStringBuilder(); private final List mCharacterStyles = new ArrayList<>(); private int mCaptionWindowId; private int mRow = -1; private float mFontScale; private float mTextSize; private String mWidestChar; private int mLastCaptionLayoutWidth; private int mLastCaptionLayoutHeight; public CCWindowLayout(Context context) { this(context, null); } public CCWindowLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CCWindowLayout(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public CCWindowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); // Add a subtitle view to the layout. mCCView = new CCView(context); LayoutParams params = new RelativeLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); addView(mCCView, params); // Set the system wide CC preferences to the subtitle view. CaptioningManager captioningManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); mFontScale = captioningManager.getFontScale(); setCaptionStyle(captioningManager.getUserStyle()); mCCView.setText(""); updateWidestChar(); } public void setCaptionStyle(CaptionStyle style) { mCaptionStyle = style; mCCView.setCaptionStyle(style); } public void setFontScale(float fontScale) { mFontScale = fontScale; updateTextSize(); } public int getCaptionWindowId() { return mCaptionWindowId; } public void setCaptionWindowId(int captionWindowId) { mCaptionWindowId = captionWindowId; } public void clear() { clearText(); hide(); } public void show() { setVisibility(View.VISIBLE); requestLayout(); } public void hide() { setVisibility(View.INVISIBLE); requestLayout(); } public void setPenAttr(Cea708CCParser.CaptionPenAttr penAttr) { mCharacterStyles.clear(); if (penAttr.italic) { mCharacterStyles.add(new StyleSpan(Typeface.ITALIC)); } if (penAttr.underline) { mCharacterStyles.add(new UnderlineSpan()); } switch (penAttr.penSize) { case Cea708CCParser.CaptionPenAttr.PEN_SIZE_SMALL: mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_SMALL)); break; case Cea708CCParser.CaptionPenAttr.PEN_SIZE_LARGE: mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_LARGE)); break; } switch (penAttr.penOffset) { case Cea708CCParser.CaptionPenAttr.OFFSET_SUBSCRIPT: mCharacterStyles.add(new SubscriptSpan()); break; case Cea708CCParser.CaptionPenAttr.OFFSET_SUPERSCRIPT: mCharacterStyles.add(new SuperscriptSpan()); break; } } public void setPenColor(Cea708CCParser.CaptionPenColor penColor) { // TODO: apply pen colors or skip this and use the style of system wide CC style as is. } public void setPenLocation(int row, int column) { // TODO: change the location of pen based on row and column both. if (mRow >= 0) { for (int r = mRow; r < row; ++r) { appendText("\n"); } } mRow = row; } public void setWindowAttr(Cea708CCParser.CaptionWindowAttr windowAttr) { // TODO: apply window attrs or skip this and use the style of system wide CC style as // is. } public void sendBuffer(String buffer) { appendText(buffer); } public void sendControl(char control) { // TODO: there are a bunch of ASCII-style control codes. } /** * This method places the window on a given CaptionLayout along with the anchor of the * window. *

* According to CEA-708B, the anchor id indicates the gravity of the window as the follows. * For example, A value 7 of a anchor id says that a window is align with its parent bottom * and is located at the center horizontally of its parent. *

*

Anchor id and the gravity of a window

* * * * * * * * * * * * * * * * * * * * * * * * * *
GRAVITYLEFTCENTER_HORIZONTALRIGHT
TOP012
CENTER_VERTICAL345
BOTTOM678
*

* In order to handle the gravity of a window, there are two steps. First, set the size of * the window. Since the window will be positioned at ScaledLayout, the size factors are * determined in a ratio. Second, set the gravity of the window. CaptionWindowLayout is * inherited from RelativeLayout. Hence, we could set the gravity of its child view, * SubtitleView. *

*

* The gravity of the window is also related to its size. When it should be pushed to a one * of the end of the window, like LEFT, RIGHT, TOP or BOTTOM, the anchor point should be a * boundary of the window. When it should be pushed in the horizontal/vertical center of its * container, the horizontal/vertical center point of the window should be the same as the * anchor point. *

* * @param ccLayout a given CaptionLayout, which contains a safe title area. * @param captionWindow a given CaptionWindow, which stores the construction info of the * window. */ public void initWindow(CCLayout ccLayout, Cea708CCParser.CaptionWindow captionWindow) { if (mCCLayout != ccLayout) { if (mCCLayout != null) { mCCLayout.removeOnLayoutChangeListener(this); } mCCLayout = ccLayout; mCCLayout.addOnLayoutChangeListener(this); updateWidestChar(); } // Both anchor vertical and horizontal indicates the position cell number of the window. float scaleRow = (float) captionWindow.anchorVertical / (captionWindow.relativePositioning ? ANCHOR_RELATIVE_POSITIONING_MAX : ANCHOR_VERTICAL_MAX); // Assumes it has a wide aspect ratio track. float scaleCol = (float) captionWindow.anchorHorizontal / (captionWindow.relativePositioning ? ANCHOR_RELATIVE_POSITIONING_MAX : ANCHOR_HORIZONTAL_16_9_MAX); // The range of scaleRow/Col need to be verified to be in [0, 1]. // Otherwise a RuntimeException will be raised in ScaledLayout. if (scaleRow < 0 || scaleRow > 1) { Log.i(TAG, "The vertical position of the anchor point should be at the range of 0 " + "and 1 but " + scaleRow); scaleRow = Math.max(0, Math.min(scaleRow, 1)); } if (scaleCol < 0 || scaleCol > 1) { Log.i(TAG, "The horizontal position of the anchor point should be at the range of 0" + " and 1 but " + scaleCol); scaleCol = Math.max(0, Math.min(scaleCol, 1)); } int gravity = Gravity.CENTER; int horizontalMode = captionWindow.anchorId % ANCHOR_MODE_DIVIDER; int verticalMode = captionWindow.anchorId / ANCHOR_MODE_DIVIDER; float scaleStartRow = 0; float scaleEndRow = 1; float scaleStartCol = 0; float scaleEndCol = 1; switch (horizontalMode) { case ANCHOR_HORIZONTAL_MODE_LEFT: gravity = Gravity.LEFT; mCCView.setAlignment(Alignment.ALIGN_NORMAL); scaleStartCol = scaleCol; break; case ANCHOR_HORIZONTAL_MODE_CENTER: float gap = Math.min(1 - scaleCol, scaleCol); // Since all TV sets use left text alignment instead of center text alignment // for this case, we follow the industry convention if possible. int columnCount = captionWindow.columnCount + 1; columnCount = Math.min(getScreenColumnCount(), columnCount); StringBuilder widestTextBuilder = new StringBuilder(); for (int i = 0; i < columnCount; ++i) { widestTextBuilder.append(mWidestChar); } Paint paint = new Paint(); paint.setTypeface(mCaptionStyle.getTypeface()); paint.setTextSize(mTextSize); float maxWindowWidth = paint.measureText(widestTextBuilder.toString()); float halfMaxWidthScale = mCCLayout.getWidth() > 0 ? maxWindowWidth / 2.0f / (mCCLayout.getWidth() * 0.8f) : 0.0f; if (halfMaxWidthScale > 0f && halfMaxWidthScale < scaleCol) { // Calculate the expected max window size based on the column count of the // caption window multiplied by average alphabets char width, then align the // left side of the window with the left side of the expected max window. gravity = Gravity.LEFT; mCCView.setAlignment(Alignment.ALIGN_NORMAL); scaleStartCol = scaleCol - halfMaxWidthScale; scaleEndCol = 1.0f; } else { // The gap will be the minimum distance value of the distances from both // horizontal end points to the anchor point. // If scaleCol <= 0.5, the range of scaleCol is [0, the anchor point * 2]. // If scaleCol > 0.5, the range of scaleCol is // [(1 - the anchor point) * 2, 1]. // The anchor point is located at the horizontal center of the window in // both cases. gravity = Gravity.CENTER_HORIZONTAL; mCCView.setAlignment(Alignment.ALIGN_CENTER); scaleStartCol = scaleCol - gap; scaleEndCol = scaleCol + gap; } break; case ANCHOR_HORIZONTAL_MODE_RIGHT: gravity = Gravity.RIGHT; mCCView.setAlignment(Alignment.ALIGN_RIGHT); scaleEndCol = scaleCol; break; } switch (verticalMode) { case ANCHOR_VERTICAL_MODE_TOP: gravity |= Gravity.TOP; scaleStartRow = scaleRow; break; case ANCHOR_VERTICAL_MODE_CENTER: gravity |= Gravity.CENTER_VERTICAL; // See the above comment. float gap = Math.min(1 - scaleRow, scaleRow); scaleStartRow = scaleRow - gap; scaleEndRow = scaleRow + gap; break; case ANCHOR_VERTICAL_MODE_BOTTOM: gravity |= Gravity.BOTTOM; scaleEndRow = scaleRow; break; } mCCLayout.addOrUpdateViewToSafeTitleArea(this, new ScaledLayout .ScaledLayoutParams(scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol)); setCaptionWindowId(captionWindow.id); setRowLimit(captionWindow.rowCount); setGravity(gravity); if (captionWindow.visible) { show(); } else { hide(); } } @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { int width = right - left; int height = bottom - top; if (width != mLastCaptionLayoutWidth || height != mLastCaptionLayoutHeight) { mLastCaptionLayoutWidth = width; mLastCaptionLayoutHeight = height; updateTextSize(); } } private void updateWidestChar() { Paint paint = new Paint(); paint.setTypeface(mCaptionStyle.getTypeface()); Charset latin1 = Charset.forName("ISO-8859-1"); float widestCharWidth = 0f; for (int i = 0; i < 256; ++i) { String ch = new String(new byte[]{(byte) i}, latin1); float charWidth = paint.measureText(ch); if (widestCharWidth < charWidth) { widestCharWidth = charWidth; mWidestChar = ch; } } updateTextSize(); } private void updateTextSize() { if (mCCLayout == null) return; // Calculate text size based on the max window size. StringBuilder widestTextBuilder = new StringBuilder(); int screenColumnCount = getScreenColumnCount(); for (int i = 0; i < screenColumnCount; ++i) { widestTextBuilder.append(mWidestChar); } String widestText = widestTextBuilder.toString(); Paint paint = new Paint(); paint.setTypeface(mCaptionStyle.getTypeface()); float startFontSize = 0f; float endFontSize = 255f; while (startFontSize < endFontSize) { float testTextSize = (startFontSize + endFontSize) / 2f; paint.setTextSize(testTextSize); float width = paint.measureText(widestText); if (mCCLayout.getWidth() * 0.8f > width) { startFontSize = testTextSize + 0.01f; } else { endFontSize = testTextSize - 0.01f; } } mTextSize = endFontSize * mFontScale; mCCView.setTextSize(mTextSize); } private int getScreenColumnCount() { // Assume it has a wide aspect ratio track. return MAX_COLUMN_COUNT_16_9; } public void removeFromCaptionView() { if (mCCLayout != null) { mCCLayout.removeViewFromSafeTitleArea(this); mCCLayout.removeOnLayoutChangeListener(this); mCCLayout = null; } } public void setText(String text) { updateText(text, false); } public void appendText(String text) { updateText(text, true); } public void clearText() { mBuilder.clear(); mCCView.setText(""); } private void updateText(String text, boolean appended) { if (!appended) { mBuilder.clear(); } if (text != null && text.length() > 0) { int length = mBuilder.length(); mBuilder.append(text); for (CharacterStyle characterStyle : mCharacterStyles) { mBuilder.setSpan(characterStyle, length, mBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } String[] lines = TextUtils.split(mBuilder.toString(), "\n"); // Truncate text not to exceed the row limit. // Plus one here since the range of the rows is [0, mRowLimit]. String truncatedText = TextUtils.join("\n", Arrays.copyOfRange( lines, Math.max(0, lines.length - (mRowLimit + 1)), lines.length)); mBuilder.delete(0, mBuilder.length() - truncatedText.length()); // Trim the buffer first then set text to CCView. int start = 0, last = mBuilder.length() - 1; int end = last; while ((start <= end) && (mBuilder.charAt(start) <= ' ')) { ++start; } while ((end >= start) && (mBuilder.charAt(end) <= ' ')) { --end; } if (start == 0 && end == last) { mCCView.setText(mBuilder); } else { SpannableStringBuilder trim = new SpannableStringBuilder(); trim.append(mBuilder); if (end < last) { trim.delete(end + 1, last + 1); } if (start > 0) { trim.delete(0, start); } mCCView.setText(trim); } } public void setRowLimit(int rowLimit) { if (rowLimit < 0) { throw new IllegalArgumentException("A rowLimit should have a positive number"); } mRowLimit = rowLimit; } } /** @hide */ static class CCView extends SubtitleView { private static final CaptionStyle DEFAULT_CAPTION_STYLE = CaptionStyle.DEFAULT; public CCView(Context context) { this(context, null); } public CCView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CCView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public CCView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } public void setCaptionStyle(CaptionStyle style) { setForegroundColor(style.hasForegroundColor() ? style.foregroundColor : DEFAULT_CAPTION_STYLE.foregroundColor); setBackgroundColor(style.hasBackgroundColor() ? style.backgroundColor : DEFAULT_CAPTION_STYLE.backgroundColor); setEdgeType(style.hasEdgeType() ? style.edgeType : DEFAULT_CAPTION_STYLE.edgeType); setEdgeColor(style.hasEdgeColor() ? style.edgeColor : DEFAULT_CAPTION_STYLE.edgeColor); setTypeface(style.getTypeface()); } } }