/* * 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 com.android.tv.tuner.cc; import android.os.SystemClock; import android.support.annotation.IntDef; import android.util.Log; import android.util.SparseIntArray; import com.android.tv.tuner.data.Cea708Data; import com.android.tv.tuner.data.Cea708Data.CaptionColor; import com.android.tv.tuner.data.Cea708Data.CaptionEvent; import com.android.tv.tuner.data.Cea708Data.CaptionPenAttr; import com.android.tv.tuner.data.Cea708Data.CaptionPenColor; import com.android.tv.tuner.data.Cea708Data.CaptionPenLocation; import com.android.tv.tuner.data.Cea708Data.CaptionWindow; import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr; import com.android.tv.tuner.data.Cea708Data.CcPacket; import com.android.tv.tuner.util.ByteArrayBuffer; import java.io.UnsupportedEncodingException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.TreeSet; /** * 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. user_data -> CcPacket ({@link #parseClosedCaption} method)

* *

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 ({@link #parseCcPacket} method)

* *

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 ({@link #parseDtvCcPacket} method)

* *

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 {@link #mListenServiceNumber}. 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. */ public class Cea708Parser { private static final String TAG = "Cea708Parser"; private static final boolean DEBUG = false; // According to CEA-708B, the maximum value of closed caption bandwidth is 9600bps. private static final int MAX_ALLOCATED_SIZE = 9600 / 8; private static final String MUSIC_NOTE_CHAR = new String("\u266B".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); // The following values are denoting the type of closed caption data. // See CEA-708B section 4.4.1. private static final int CC_TYPE_DTVCC_PACKET_START = 3; private static final int CC_TYPE_DTVCC_PACKET_DATA = 2; // The following values are defined in CEA-708B Figure 4 and 6. private static final int DTVCC_MAX_PACKET_SIZE = 64; private static final int DTVCC_PACKET_SIZE_SCALE_FACTOR = 2; private static final int DTVCC_EXTENDED_SERVICE_NUMBER_POINT = 7; // The following values are for seeking closed caption tracks. private static final int DISCOVERY_PERIOD_MS = 10000; // 10 sec private static final int DISCOVERY_NUM_BYTES_THRESHOLD = 10; // 10 bytes private static final int DISCOVERY_CC_SERVICE_NUMBER_START = 1; // CC1 private static final int DISCOVERY_CC_SERVICE_NUMBER_END = 4; // CC4 private final ByteArrayBuffer mDtvCcPacket = new ByteArrayBuffer(MAX_ALLOCATED_SIZE); private final TreeSet mCcPackets = new TreeSet<>(); private final StringBuffer mBuffer = new StringBuffer(); private final SparseIntArray mDiscoveredNumBytes = new SparseIntArray(); // per service number private long mLastDiscoveryLaunchedMs = SystemClock.elapsedRealtime(); private int mCommand = 0; private int mListenServiceNumber = 0; private boolean mDtvCcPacking = false; private boolean mFirstServiceNumberDiscovered; // Assign a dummy listener in order to avoid null checks. private OnCea708ParserListener mListener = new OnCea708ParserListener() { @Override public void emitEvent(CaptionEvent event) { // do nothing } @Override public void discoverServiceNumber(int serviceNumber) { // do nothing } }; /** * {@link Cea708Parser} emits caption event of three different types. {@link * OnCea708ParserListener#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. * *

*/ @IntDef({ CAPTION_EMIT_TYPE_BUFFER, CAPTION_EMIT_TYPE_CONTROL, CAPTION_EMIT_TYPE_COMMAND_CWX, CAPTION_EMIT_TYPE_COMMAND_CLW, CAPTION_EMIT_TYPE_COMMAND_DSW, CAPTION_EMIT_TYPE_COMMAND_HDW, CAPTION_EMIT_TYPE_COMMAND_TGW, CAPTION_EMIT_TYPE_COMMAND_DLW, CAPTION_EMIT_TYPE_COMMAND_DLY, CAPTION_EMIT_TYPE_COMMAND_DLC, CAPTION_EMIT_TYPE_COMMAND_RST, CAPTION_EMIT_TYPE_COMMAND_SPA, CAPTION_EMIT_TYPE_COMMAND_SPC, CAPTION_EMIT_TYPE_COMMAND_SPL, CAPTION_EMIT_TYPE_COMMAND_SWA, CAPTION_EMIT_TYPE_COMMAND_DFX }) @Retention(RetentionPolicy.SOURCE) public @interface CaptionEmitType {} 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; public interface OnCea708ParserListener { void emitEvent(CaptionEvent event); void discoverServiceNumber(int serviceNumber); } public void setListener(OnCea708ParserListener listener) { if (listener != null) { mListener = listener; } } public void clear() { mDtvCcPacket.clear(); mCcPackets.clear(); mBuffer.setLength(0); mDiscoveredNumBytes.clear(); mCommand = 0; mDtvCcPacking = false; } public void setListenServiceNumber(int serviceNumber) { mListenServiceNumber = serviceNumber; } 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 1. user_data -> CcPacket ({@link #parseClosedCaption} method) public void parseClosedCaption(ByteBuffer data, long framePtsUs) { int ccCount = data.limit() / 3; byte[] ccBytes = new byte[3 * ccCount]; for (int i = 0; i < 3 * ccCount; i++) { ccBytes[i] = data.get(i); } CcPacket ccPacket = new CcPacket(ccBytes, ccCount, framePtsUs); mCcPackets.add(ccPacket); } public boolean processClosedCaptions(long framePtsUs) { // To get the sorted cc packets that have lower frame pts than current frame pts, // the following offset divides off the lower side of the packets. CcPacket offsetPacket = new CcPacket(new byte[0], 0, framePtsUs); offsetPacket = mCcPackets.lower(offsetPacket); boolean processed = false; if (offsetPacket != null) { while (!mCcPackets.isEmpty() && offsetPacket.compareTo(mCcPackets.first()) >= 0) { CcPacket packet = mCcPackets.pollFirst(); parseCcPacket(packet); processed = true; } } return processed; } // Step 2. CcPacket -> DTVCC packet ({@link #parseCcPacket} method) private void parseCcPacket(CcPacket ccPacket) { // For the details of cc packet, see ATSC TSG-676 - Table A8. byte[] bytes = ccPacket.bytes; int pos = 0; for (int i = 0; i < ccPacket.ccCount; ++i) { boolean ccValid = (bytes[pos] & 0x04) != 0; int ccType = bytes[pos] & 0x03; // The dtvcc should be considered complete: // - if either ccValid is set and ccType is 3 // - or ccValid is clear and ccType is 2 or 3. if (ccValid) { if (ccType == CC_TYPE_DTVCC_PACKET_START) { if (mDtvCcPacking) { parseDtvCcPacket(mDtvCcPacket.buffer(), mDtvCcPacket.length()); mDtvCcPacket.clear(); } mDtvCcPacking = true; mDtvCcPacket.append(bytes[pos + 1]); mDtvCcPacket.append(bytes[pos + 2]); } else if (mDtvCcPacking && ccType == CC_TYPE_DTVCC_PACKET_DATA) { mDtvCcPacket.append(bytes[pos + 1]); mDtvCcPacket.append(bytes[pos + 2]); } } else { if ((ccType == CC_TYPE_DTVCC_PACKET_START || ccType == CC_TYPE_DTVCC_PACKET_DATA) && mDtvCcPacking) { mDtvCcPacking = false; parseDtvCcPacket(mDtvCcPacket.buffer(), mDtvCcPacket.length()); mDtvCcPacket.clear(); } } pos += 3; } } // Step 3. DTVCC packet -> Service Blocks ({@link #parseDtvCcPacket} method) private void parseDtvCcPacket(byte[] data, int limit) { // For the details of DTVCC packet, see CEA-708B Figure 4. int pos = 0; int packetSize = data[pos] & 0x3f; if (packetSize == 0) { packetSize = DTVCC_MAX_PACKET_SIZE; } int calculatedPacketSize = packetSize * DTVCC_PACKET_SIZE_SCALE_FACTOR; if (limit != calculatedPacketSize) { return; } ++pos; int len = pos + calculatedPacketSize; while (pos < len) { // For the details of Service Block, see CEA-708B Figure 5 and 6. int serviceNumber = (data[pos] & 0xe0) >> 5; int blockSize = data[pos] & 0x1f; ++pos; if (serviceNumber == DTVCC_EXTENDED_SERVICE_NUMBER_POINT) { serviceNumber = (data[pos] & 0x3f); ++pos; // Return if invalid service number if (serviceNumber < DTVCC_EXTENDED_SERVICE_NUMBER_POINT) { return; } } if (pos + blockSize > limit) { return; } // Send parsed service number in order to find unveiled closed caption tracks which // are not specified in any ATSC PSIP sections. Since some broadcasts send empty closed // caption tracks, it detects the proper closed caption tracks by counting the number of // bytes sent with the same service number during a discovery period. // The viewer in most TV sets chooses between CC1, CC2, CC3, CC4 to view different // language captions. Therefore, only CC1, CC2, CC3, CC4 are allowed to be reported. if (blockSize > 0 && serviceNumber >= DISCOVERY_CC_SERVICE_NUMBER_START && serviceNumber <= DISCOVERY_CC_SERVICE_NUMBER_END) { mDiscoveredNumBytes.put( serviceNumber, blockSize + mDiscoveredNumBytes.get(serviceNumber, 0)); } if (mLastDiscoveryLaunchedMs + DISCOVERY_PERIOD_MS < SystemClock.elapsedRealtime() || !mFirstServiceNumberDiscovered) { for (int i = 0; i < mDiscoveredNumBytes.size(); ++i) { int discoveredNumBytes = mDiscoveredNumBytes.valueAt(i); if (discoveredNumBytes >= DISCOVERY_NUM_BYTES_THRESHOLD) { int discoveredServiceNumber = mDiscoveredNumBytes.keyAt(i); mListener.discoverServiceNumber(discoveredServiceNumber); mFirstServiceNumberDiscovered = true; } } mDiscoveredNumBytes.clear(); mLastDiscoveryLaunchedMs = SystemClock.elapsedRealtime(); } // Skip current service block if either there is no block data or the service number // is not same as listening service number. if (blockSize == 0 || serviceNumber != mListenServiceNumber) { pos += blockSize; continue; } // From this point, starts to read DTVCC coding layer. // First, identify code groups, which is defined in CEA-708B Section 7.1. int blockLimit = pos + blockSize; while (pos < blockLimit) { pos = parseServiceBlockData(data, pos); } // Emit the buffer after reading codes. emitCaptionBuffer(); pos = blockLimit; } } // 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 == Cea708Data.CODE_C0_EXT1) { pos = parseExt1(data, pos); } else if (mCommand >= Cea708Data.CODE_C0_RANGE_START && mCommand <= Cea708Data.CODE_C0_RANGE_END) { pos = parseC0(data, pos); } else if (mCommand >= Cea708Data.CODE_C1_RANGE_START && mCommand <= Cea708Data.CODE_C1_RANGE_END) { pos = parseC1(data, pos); } else if (mCommand >= Cea708Data.CODE_G0_RANGE_START && mCommand <= Cea708Data.CODE_G0_RANGE_END) { pos = parseG0(data, pos); } else if (mCommand >= Cea708Data.CODE_G1_RANGE_START && mCommand <= Cea708Data.CODE_G1_RANGE_END) { 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 >= Cea708Data.CODE_C0_SKIP2_RANGE_START && mCommand <= Cea708Data.CODE_C0_SKIP2_RANGE_END) { if (mCommand == Cea708Data.CODE_C0_P16) { // TODO : P16 escapes next two bytes for the large character maps.(no standard rule) // TODO : 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 >= Cea708Data.CODE_C0_SKIP1_RANGE_START && mCommand <= Cea708Data.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 Cea708Data.CODE_C0_NUL: break; case Cea708Data.CODE_C0_ETX: emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); break; case Cea708Data.CODE_C0_BS: emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); break; case Cea708Data.CODE_C0_FF: emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); break; case Cea708Data.CODE_C0_CR: mBuffer.append('\n'); break; case Cea708Data.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 Cea708Data.CODE_C1_CW0: case Cea708Data.CODE_C1_CW1: case Cea708Data.CODE_C1_CW2: case Cea708Data.CODE_C1_CW3: case Cea708Data.CODE_C1_CW4: case Cea708Data.CODE_C1_CW5: case Cea708Data.CODE_C1_CW6: case Cea708Data.CODE_C1_CW7: { // SetCurrentWindow0-7 int windowId = mCommand - Cea708Data.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 Cea708Data.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 Cea708Data.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 Cea708Data.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 Cea708Data.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 Cea708Data.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 Cea708Data.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 Cea708Data.CODE_C1_DLC: { // DelayCancel emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLC, null)); if (DEBUG) { Log.d(TAG, "CaptionCommand DLC"); } break; } case Cea708Data.CODE_C1_RST: { // Reset emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_RST, null)); if (DEBUG) { Log.d(TAG, "CaptionCommand RST"); } break; } case Cea708Data.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 Cea708Data.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 Cea708Data.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 Cea708Data.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 Cea708Data.CODE_C1_DF0: case Cea708Data.CODE_C1_DF1: case Cea708Data.CODE_C1_DF2: case Cea708Data.CODE_C1_DF3: case Cea708Data.CODE_C1_DF4: case Cea708Data.CODE_C1_DF5: case Cea708Data.CODE_C1_DF6: case Cea708Data.CODE_C1_DF7: { // DefineWindow0-7 int windowId = mCommand - Cea708Data.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 == Cea708Data.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 >= Cea708Data.CODE_C2_RANGE_START && mCommand <= Cea708Data.CODE_C2_RANGE_END) { pos = parseC2(data, pos); } else if (mCommand >= Cea708Data.CODE_C3_RANGE_START && mCommand <= Cea708Data.CODE_C3_RANGE_END) { pos = parseC3(data, pos); } else if (mCommand >= Cea708Data.CODE_G2_RANGE_START && mCommand <= Cea708Data.CODE_G2_RANGE_END) { pos = parseG2(data, pos); } else if (mCommand >= Cea708Data.CODE_G3_RANGE_START && mCommand <= Cea708Data.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 >= Cea708Data.CODE_C2_SKIP0_RANGE_START && mCommand <= Cea708Data.CODE_C2_SKIP0_RANGE_END) { // Do nothing. } else if (mCommand >= Cea708Data.CODE_C2_SKIP1_RANGE_START && mCommand <= Cea708Data.CODE_C2_SKIP1_RANGE_END) { ++pos; } else if (mCommand >= Cea708Data.CODE_C2_SKIP2_RANGE_START && mCommand <= Cea708Data.CODE_C2_SKIP2_RANGE_END) { pos += 2; } else if (mCommand >= Cea708Data.CODE_C2_SKIP3_RANGE_START && mCommand <= Cea708Data.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 >= Cea708Data.CODE_C3_SKIP4_RANGE_START && mCommand <= Cea708Data.CODE_C3_SKIP4_RANGE_END) { pos += 4; } else if (mCommand >= Cea708Data.CODE_C3_SKIP5_RANGE_START && mCommand <= Cea708Data.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 Cea708Data.CODE_G2_TSP: // TODO : TSP is the Transparent space break; case Cea708Data.CODE_G2_NBTSP: // TODO : NBTSP is Non-Breaking Transparent Space. break; case Cea708Data.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 == Cea708Data.CODE_G3_CC) { // TODO : [CC] icon with square corners } // Do nothing return pos; } }