| /* |
| * 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<Cue> 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. |
| * |
| * <p>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. |
| * |
| * <p>There are 4 steps to decode user_data to provide closed caption services. Step 1 and 2 are |
| * done in NuPlayer and libstagefright. |
| * |
| * <h3>Step 1. user_data -> CcPacket</h3> |
| * |
| * <p>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. |
| * |
| * <h3>Step 2. CcPacket -> DTVCC packet</h3> |
| * |
| * <p>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. |
| * |
| * <h3>Step 3. DTVCC packet -> Service Blocks</h3> |
| * |
| * <p>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. |
| * |
| * <h3>Step 4. Interpreting Service Block Data ({@link #parseServiceBlockData}, {@code parseXX}, |
| * and {@link #parseExt1} methods)</h3> |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>4 main code groups: C0, C1, G0, G1 |
| * <br>4 extended code groups: C2, C3, G2, G3 |
| * |
| * <p>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. |
| * |
| * <p>The main code groups: |
| * <ul> |
| * <li>C0 - contains modified ASCII control codes. It is not intended by CEA-708 but Korea TTA |
| * standard for ATSC CC uses P16 character heavily, which is unclear entity in CEA-708 doc, |
| * even for the alphanumeric characters instead of ASCII characters.</li> |
| * <li>C1 - contains the caption commands. There are 3 categories of a caption command.</li> |
| * <ul> |
| * <li>Window commands: The window commands control a caption window which is addressable area being |
| * with in the Safe title area. (CWX, CLW, DSW, HDW, TGW, DLW, SWA, DFX)</li> |
| * <li>Pen commands: Th pen commands control text style and location. (SPA, SPC, SPL)</li> |
| * <li>Job commands: The job commands make a delay and recover from the delay. (DLY, DLC, RST)</li> |
| * </ul> |
| * <li>G0 - same as printable ASCII character set except music note character.</li> |
| * <li>G1 - same as ISO 8859-1 Latin 1 character set.</li> |
| * </ul> |
| * <p>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 placeholder 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 . |
| * |
| * <p>{@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. |
| * |
| * <ul><li>{@code CAPTION_EMIT_TYPE_BUFFER}: Passes a caption text buffer to a observer. |
| * {@code obj} must be of {@link String}.</li> |
| * |
| * <li>{@code CAPTION_EMIT_TYPE_CONTROL}: Passes a caption character control code to a observer. |
| * {@code obj} must be of {@link Character}.</li> |
| * |
| * <li>{@code CAPTION_EMIT_TYPE_CLEAR_COMMAND}: Passes a clear command to a observer. |
| * {@code obj} must be {@code NULL}.</li></ul> |
| */ |
| 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<Rect> mRectTopLeftSorter = new Comparator<Rect>() { |
| @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. |
| * <br> |
| * Vertical coordinate system: (scaleStartRow * 100) % ~ (scaleEndRow * 100) % |
| * Horizontal coordinate system: (scaleStartCol * 100) % ~ (scaleEndCol * 100) % |
| * <br> |
| * In XML, for example, |
| * <pre> |
| * {@code |
| * <View |
| * app:layout_scaleStartRow="0.1" |
| * app:layout_scaleEndRow="0.5" |
| * app:layout_scaleStartCol="0.4" |
| * app:layout_scaleEndCol="1" /> |
| * } |
| * </pre> |
| */ |
| 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. |
| * |
| * <p>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<Cea708CCParser.CaptionEvent> 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<CCWindowLayout> getWindowsFromBitmap(int windowBitmap) { |
| ArrayList<CCWindowLayout> 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<CharacterStyle> 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. |
| * <p> |
| * 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. |
| * </p> |
| * <h4>Anchor id and the gravity of a window</h4> |
| * <table> |
| * <tr> |
| * <th>GRAVITY</th> |
| * <th>LEFT</th> |
| * <th>CENTER_HORIZONTAL</th> |
| * <th>RIGHT</th> |
| * </tr> |
| * <tr> |
| * <th>TOP</th> |
| * <td>0</td> |
| * <td>1</td> |
| * <td>2</td> |
| * </tr> |
| * <tr> |
| * <th>CENTER_VERTICAL</th> |
| * <td>3</td> |
| * <td>4</td> |
| * <td>5</td> |
| * </tr> |
| * <tr> |
| * <th>BOTTOM</th> |
| * <td>6</td> |
| * <td>7</td> |
| * <td>8</td> |
| * </tr> |
| * </table> |
| * <p> |
| * 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. |
| * </p> |
| * <p> |
| * 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. |
| * </p> |
| * |
| * @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()); |
| } |
| } |
| } |