From 978bf5eff0f5bc8a8878696bc886d8b99a29ee39 Mon Sep 17 00:00:00 2001 From: Jaesung Chung Date: Mon, 7 Dec 2015 21:46:40 +0900 Subject: [PATCH] Support for CEA-708 closed caption Bug: 24314667 Change-Id: I5e04f7646e40eb8f358666c8b109eccb6617a246 --- core/java/android/widget/VideoView.java | 2 + .../android/internal/widget/SubtitleView.java | 7 +- .../android/media/Cea708CaptionRenderer.java | 2151 +++++++++++++++++ .../android/media/ClosedCaptionRenderer.java | 384 +-- media/java/android/media/MediaPlayer.java | 6 + 5 files changed, 2381 insertions(+), 169 deletions(-) create mode 100644 media/java/android/media/Cea708CaptionRenderer.java diff --git a/core/java/android/widget/VideoView.java b/core/java/android/widget/VideoView.java index 26717394d1eea..f084db2d1589b 100644 --- a/core/java/android/widget/VideoView.java +++ b/core/java/android/widget/VideoView.java @@ -22,6 +22,7 @@ import android.content.DialogInterface; import android.content.res.Resources; import android.graphics.Canvas; import android.media.AudioManager; +import android.media.Cea708CaptionRenderer; import android.media.ClosedCaptionRenderer; import android.media.MediaFormat; import android.media.MediaPlayer; @@ -328,6 +329,7 @@ public class VideoView extends SurfaceView context, mMediaPlayer.getMediaTimeProvider(), mMediaPlayer); controller.registerRenderer(new WebVttRenderer(context)); controller.registerRenderer(new TtmlRenderer(context)); + controller.registerRenderer(new Cea708CaptionRenderer(context)); controller.registerRenderer(new ClosedCaptionRenderer(context)); mMediaPlayer.setSubtitleAnchor(controller, this); diff --git a/core/java/com/android/internal/widget/SubtitleView.java b/core/java/com/android/internal/widget/SubtitleView.java index 8c395ec027bf7..3230185dbda72 100644 --- a/core/java/com/android/internal/widget/SubtitleView.java +++ b/core/java/com/android/internal/widget/SubtitleView.java @@ -28,6 +28,7 @@ import android.graphics.Paint.Style; import android.graphics.RectF; import android.graphics.Typeface; import android.text.Layout.Alignment; +import android.text.SpannableStringBuilder; import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; @@ -54,8 +55,8 @@ public class SubtitleView extends View { /** Temporary rectangle used for computing line bounds. */ private final RectF mLineBounds = new RectF(); - /** Reusable string builder used for holding text. */ - private final StringBuilder mText = new StringBuilder(); + /** Reusable spannable string builder used for holding text. */ + private final SpannableStringBuilder mText = new SpannableStringBuilder(); private Alignment mAlignment; private TextPaint mTextPaint; @@ -141,7 +142,7 @@ public class SubtitleView extends View { } public void setText(CharSequence text) { - mText.setLength(0); + mText.clear(); mText.append(text); mHasMeasurements = false; diff --git a/media/java/android/media/Cea708CaptionRenderer.java b/media/java/android/media/Cea708CaptionRenderer.java new file mode 100644 index 0000000000000..88912fef45010 --- /dev/null +++ b/media/java/android/media/Cea708CaptionRenderer.java @@ -0,0 +1,2151 @@ +/* + * 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()); + } + } +} diff --git a/media/java/android/media/ClosedCaptionRenderer.java b/media/java/android/media/ClosedCaptionRenderer.java index 8403c1ce711d6..cc7722a03f027 100644 --- a/media/java/android/media/ClosedCaptionRenderer.java +++ b/media/java/android/media/ClosedCaptionRenderer.java @@ -23,12 +23,9 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; -import android.os.Parcel; -import android.text.ParcelableSpan; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextPaint; -import android.text.TextUtils; import android.text.style.CharacterStyle; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; @@ -52,7 +49,7 @@ import java.util.Vector; /** @hide */ public class ClosedCaptionRenderer extends SubtitleController.Renderer { private final Context mContext; - private ClosedCaptionWidget mRenderingWidget; + private Cea608CCWidget mCCWidget; public ClosedCaptionRenderer(Context context) { mContext = context; @@ -61,31 +58,35 @@ public class ClosedCaptionRenderer extends SubtitleController.Renderer { @Override public boolean supports(MediaFormat format) { if (format.containsKey(MediaFormat.KEY_MIME)) { - return format.getString(MediaFormat.KEY_MIME).equals( - MediaPlayer.MEDIA_MIMETYPE_TEXT_CEA_608); + String mimeType = format.getString(MediaFormat.KEY_MIME); + return MediaPlayer.MEDIA_MIMETYPE_TEXT_CEA_608.equals(mimeType); } return false; } @Override public SubtitleTrack createTrack(MediaFormat format) { - if (mRenderingWidget == null) { - mRenderingWidget = new ClosedCaptionWidget(mContext); + String mimeType = format.getString(MediaFormat.KEY_MIME); + if (MediaPlayer.MEDIA_MIMETYPE_TEXT_CEA_608.equals(mimeType)) { + if (mCCWidget == null) { + mCCWidget = new Cea608CCWidget(mContext); + } + return new Cea608CaptionTrack(mCCWidget, format); } - return new ClosedCaptionTrack(mRenderingWidget, format); + throw new RuntimeException("No matching format: " + format.toString()); } } /** @hide */ -class ClosedCaptionTrack extends SubtitleTrack { - private final ClosedCaptionWidget mRenderingWidget; - private final CCParser mCCParser; +class Cea608CaptionTrack extends SubtitleTrack { + private final Cea608CCParser mCCParser; + private final Cea608CCWidget mRenderingWidget; - ClosedCaptionTrack(ClosedCaptionWidget renderingWidget, MediaFormat format) { + Cea608CaptionTrack(Cea608CCWidget renderingWidget, MediaFormat format) { super(format); mRenderingWidget = renderingWidget; - mCCParser = new CCParser(renderingWidget); + mCCParser = new Cea608CCParser(mRenderingWidget); } @Override @@ -104,6 +105,149 @@ class ClosedCaptionTrack extends SubtitleTrack { } } +/** + * Abstract widget class to render a closed caption track. + * + * @hide + */ +abstract class ClosedCaptionWidget extends ViewGroup implements SubtitleTrack.RenderingWidget { + + /** @hide */ + interface ClosedCaptionLayout { + void setCaptionStyle(CaptionStyle captionStyle); + void setFontScale(float scale); + } + + private static final CaptionStyle DEFAULT_CAPTION_STYLE = CaptionStyle.DEFAULT; + + /** Captioning manager, used to obtain and track caption properties. */ + private final CaptioningManager mManager; + + /** Current caption style. */ + protected CaptionStyle mCaptionStyle; + + /** Callback for rendering changes. */ + protected OnChangedListener mListener; + + /** Concrete layout of CC. */ + protected ClosedCaptionLayout mClosedCaptionLayout; + + /** Whether a caption style change listener is registered. */ + private boolean mHasChangeListener; + + public ClosedCaptionWidget(Context context) { + this(context, null); + } + + public ClosedCaptionWidget(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyle) { + this(context, attrs, defStyle, 0); + } + + public ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + // Cannot render text over video when layer type is hardware. + setLayerType(View.LAYER_TYPE_SOFTWARE, null); + + mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); + mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(mManager.getUserStyle()); + + mClosedCaptionLayout = createCaptionLayout(context); + mClosedCaptionLayout.setCaptionStyle(mCaptionStyle); + mClosedCaptionLayout.setFontScale(mManager.getFontScale()); + addView((ViewGroup) mClosedCaptionLayout, LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT); + + requestLayout(); + } + + public abstract ClosedCaptionLayout createCaptionLayout(Context context); + + @Override + public void setOnChangedListener(OnChangedListener listener) { + mListener = listener; + } + + @Override + public void setSize(int width, int height) { + final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); + final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); + + measure(widthSpec, heightSpec); + layout(0, 0, width, height); + } + + @Override + public void setVisible(boolean visible) { + if (visible) { + setVisibility(View.VISIBLE); + } else { + setVisibility(View.GONE); + } + + manageChangeListener(); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + manageChangeListener(); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + manageChangeListener(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + ((ViewGroup) mClosedCaptionLayout).measure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + ((ViewGroup) mClosedCaptionLayout).layout(l, t, r, b); + } + + /** + * Manages whether this renderer is listening for caption style changes. + */ + private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() { + @Override + public void onUserStyleChanged(CaptionStyle userStyle) { + mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(userStyle); + mClosedCaptionLayout.setCaptionStyle(mCaptionStyle); + } + + @Override + public void onFontScaleChanged(float fontScale) { + mClosedCaptionLayout.setFontScale(fontScale); + } + }; + + private void manageChangeListener() { + final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE; + if (mHasChangeListener != needsListener) { + mHasChangeListener = needsListener; + + if (needsListener) { + mManager.addCaptioningChangeListener(mCaptioningListener); + } else { + mManager.removeCaptioningChangeListener(mCaptioningListener); + } + } + } +} + /** * @hide * @@ -113,11 +257,11 @@ class ClosedCaptionTrack extends SubtitleTrack { * display change with styled text for rendering. * */ -class CCParser { +class Cea608CCParser { public static final int MAX_ROWS = 15; public static final int MAX_COLS = 32; - private static final String TAG = "CCParser"; + private static final String TAG = "Cea608CCParser"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final int INVALID = -1; @@ -160,11 +304,11 @@ class CCParser { private CCMemory mNonDisplay = new CCMemory(); private CCMemory mTextMem = new CCMemory(); - CCParser(DisplayListener listener) { + Cea608CCParser(DisplayListener listener) { mListener = listener; } - void parse(byte[] data) { + public void parse(byte[] data) { CCData[] ccData = CCData.fromByteArray(data); for (int i = 0; i < ccData.length; i++) { @@ -184,8 +328,8 @@ class CCParser { } interface DisplayListener { - public void onDisplayChanged(SpannableStringBuilder[] styledTexts); - public CaptionStyle getCaptionStyle(); + void onDisplayChanged(SpannableStringBuilder[] styledTexts); + CaptionStyle getCaptionStyle(); } private CCMemory getMemory() { @@ -480,6 +624,33 @@ class CCParser { } } + /** + * Mutable version of BackgroundSpan to facilitate text rendering with edge styles. + * + * @hide + */ + public static class MutableBackgroundColorSpan extends CharacterStyle + implements UpdateAppearance { + private int mColor; + + public MutableBackgroundColorSpan(int color) { + mColor = color; + } + + public void setBackgroundColor(int color) { + mColor = color; + } + + public int getBackgroundColor() { + return mColor; + } + + @Override + public void updateDrawState(TextPaint ds) { + ds.bgColor = mColor; + } + } + /* CCLineBuilder keeps track of displayable chars, as well as * MidRow styles and PACs, for a single line of CC memory. * @@ -682,8 +853,7 @@ class CCParser { } SpannableStringBuilder[] getStyledText(CaptionStyle captionStyle) { - ArrayList rows = - new ArrayList(MAX_ROWS); + ArrayList rows = new ArrayList<>(MAX_ROWS); for (int i = 1; i <= MAX_ROWS; i++) { rows.add(mLines[i] != null ? mLines[i].getStyledText(captionStyle) : null); @@ -1043,128 +1213,40 @@ class CCParser { } } -/** - * Mutable version of BackgroundSpan to facilitate text rendering with edge - * styles. - * - * @hide - */ -class MutableBackgroundColorSpan extends CharacterStyle implements UpdateAppearance { - private int mColor; - - public MutableBackgroundColorSpan(int color) { - mColor = color; - } - - public void setBackgroundColor(int color) { - mColor = color; - } - - public int getBackgroundColor() { - return mColor; - } - - @Override - public void updateDrawState(TextPaint ds) { - ds.bgColor = mColor; - } -} - /** * Widget capable of rendering CEA-608 closed captions. * * @hide */ -class ClosedCaptionWidget extends ViewGroup implements - SubtitleTrack.RenderingWidget, - CCParser.DisplayListener { - private static final String TAG = "ClosedCaptionWidget"; - +class Cea608CCWidget extends ClosedCaptionWidget implements Cea608CCParser.DisplayListener { private static final Rect mTextBounds = new Rect(); private static final String mDummyText = "1234567890123456789012345678901234"; - private static final CaptionStyle DEFAULT_CAPTION_STYLE = CaptionStyle.DEFAULT; - /** Captioning manager, used to obtain and track caption properties. */ - private final CaptioningManager mManager; - - /** Callback for rendering changes. */ - private OnChangedListener mListener; - - /** Current caption style. */ - private CaptionStyle mCaptionStyle; - - /* Closed caption layout. */ - private CCLayout mClosedCaptionLayout; - - /** Whether a caption style change listener is registered. */ - private boolean mHasChangeListener; - - public ClosedCaptionWidget(Context context) { + public Cea608CCWidget(Context context) { this(context, null); } - public ClosedCaptionWidget(Context context, AttributeSet attrs) { - this(context, null, 0); + public Cea608CCWidget(Context context, AttributeSet attrs) { + this(context, attrs, 0); } - public ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); + public Cea608CCWidget(Context context, AttributeSet attrs, int defStyle) { + this(context, attrs, defStyle, 0); + } - // Cannot render text over video when layer type is hardware. - setLayerType(View.LAYER_TYPE_SOFTWARE, null); - - mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); - mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(mManager.getUserStyle()); - - mClosedCaptionLayout = new CCLayout(context); - mClosedCaptionLayout.setCaptionStyle(mCaptionStyle); - addView(mClosedCaptionLayout, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - - requestLayout(); + public Cea608CCWidget(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); } @Override - public void setOnChangedListener(OnChangedListener listener) { - mListener = listener; - } - - @Override - public void setSize(int width, int height) { - final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); - final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); - - measure(widthSpec, heightSpec); - layout(0, 0, width, height); - } - - @Override - public void setVisible(boolean visible) { - if (visible) { - setVisibility(View.VISIBLE); - } else { - setVisibility(View.GONE); - } - - manageChangeListener(); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - manageChangeListener(); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - manageChangeListener(); + public ClosedCaptionLayout createCaptionLayout(Context context) { + return new CCLayout(context); } @Override public void onDisplayChanged(SpannableStringBuilder[] styledTexts) { - mClosedCaptionLayout.update(styledTexts); + ((CCLayout) mClosedCaptionLayout).update(styledTexts); if (mListener != null) { mListener.onChanged(this); @@ -1176,41 +1258,6 @@ class ClosedCaptionWidget extends ViewGroup implements return mCaptionStyle; } - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - mClosedCaptionLayout.measure(widthMeasureSpec, heightMeasureSpec); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - mClosedCaptionLayout.layout(l, t, r, b); - } - - /** - * Manages whether this renderer is listening for caption style changes. - */ - private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() { - @Override - public void onUserStyleChanged(CaptionStyle userStyle) { - mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(userStyle); - mClosedCaptionLayout.setCaptionStyle(mCaptionStyle); - } - }; - - private void manageChangeListener() { - final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE; - if (mHasChangeListener != needsListener) { - mHasChangeListener = needsListener; - - if (needsListener) { - mManager.addCaptioningChangeListener(mCaptioningListener); - } else { - mManager.removeCaptioningChangeListener(mCaptioningListener); - } - } - } - private static class CCLineBox extends TextView { private static final float FONT_PADDING_RATIO = 0.75f; private static final float EDGE_OUTLINE_RATIO = 0.1f; @@ -1260,8 +1307,7 @@ class ClosedCaptionWidget extends ViewGroup implements @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - float fontSize = MeasureSpec.getSize(heightMeasureSpec) - * FONT_PADDING_RATIO; + float fontSize = MeasureSpec.getSize(heightMeasureSpec) * FONT_PADDING_RATIO; setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize); mOutlineWidth = EDGE_OUTLINE_RATIO * fontSize + 1.0f; @@ -1358,8 +1404,8 @@ class ClosedCaptionWidget extends ViewGroup implements CharSequence text = getText(); if (text instanceof Spannable) { Spannable spannable = (Spannable) text; - MutableBackgroundColorSpan[] bgSpans = spannable.getSpans( - 0, spannable.length(), MutableBackgroundColorSpan.class); + Cea608CCParser.MutableBackgroundColorSpan[] bgSpans = spannable.getSpans( + 0, spannable.length(), Cea608CCParser.MutableBackgroundColorSpan.class); for (int i = 0; i < bgSpans.length; i++) { bgSpans[i].setBackgroundColor(color); } @@ -1367,8 +1413,8 @@ class ClosedCaptionWidget extends ViewGroup implements } } - private static class CCLayout extends LinearLayout { - private static final int MAX_ROWS = CCParser.MAX_ROWS; + private static class CCLayout extends LinearLayout implements ClosedCaptionLayout { + private static final int MAX_ROWS = Cea608CCParser.MAX_ROWS; private static final float SAFE_AREA_RATIO = 0.9f; private final CCLineBox[] mLineBoxes = new CCLineBox[MAX_ROWS]; @@ -1383,12 +1429,18 @@ class ClosedCaptionWidget extends ViewGroup implements } } - void setCaptionStyle(CaptionStyle captionStyle) { + @Override + public void setCaptionStyle(CaptionStyle captionStyle) { for (int i = 0; i < MAX_ROWS; i++) { mLineBoxes[i].setCaptionStyle(captionStyle); } } + @Override + public void setFontScale(float fontScale) { + // Ignores the font scale changes of the system wide CC preference. + } + void update(SpannableStringBuilder[] textBuffer) { for (int i = 0; i < MAX_ROWS; i++) { if (textBuffer[i] != null) { @@ -1455,4 +1507,4 @@ class ClosedCaptionWidget extends ViewGroup implements } } } -}; +} diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java index bc1aedad10481..aff68f4914a8d 100644 --- a/media/java/android/media/MediaPlayer.java +++ b/media/java/android/media/MediaPlayer.java @@ -2124,6 +2124,12 @@ public class MediaPlayer implements SubtitleController.Listener */ public static final String MEDIA_MIMETYPE_TEXT_CEA_608 = "text/cea-608"; + /** + * MIME type for CEA-708 closed caption data. + * @hide + */ + public static final String MEDIA_MIMETYPE_TEXT_CEA_708 = "text/cea-708"; + /* * A helper function to check if the mime type is supported by media framework. */