diff options
Diffstat (limited to 'tuner/src/com/android/tv/tuner/cc/Cea708Parser.java')
-rw-r--r-- | tuner/src/com/android/tv/tuner/cc/Cea708Parser.java | 922 |
1 files changed, 922 insertions, 0 deletions
diff --git a/tuner/src/com/android/tv/tuner/cc/Cea708Parser.java b/tuner/src/com/android/tv/tuner/cc/Cea708Parser.java new file mode 100644 index 00000000..4e080276 --- /dev/null +++ b/tuner/src/com/android/tv/tuner/cc/Cea708Parser.java @@ -0,0 +1,922 @@ +/* + * 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. + * + * <p>ATSC DTV closed caption data are carried on picture user data of video streams. This class + * starts to parse from picture user data payload, so extraction process of user_data from video + * streams is up to outside of this code. + * + * <p>There are 4 steps to decode user_data to provide closed caption services. + * + * <h3>Step 1. user_data -> CcPacket ({@link #parseClosedCaption} method)</h3> + * + * <p>First, user_data consists of cc_data packets, which are 3-byte segments. Here, CcPacket is a + * collection of cc_data packets in a frame along with same presentation timestamp. Because cc_data + * packets must be reassembled in the frame display order, CcPackets are reordered. + * + * <h3>Step 2. CcPacket -> DTVCC packet ({@link #parseCcPacket} method)</h3> + * + * <p>Each cc_data packet has a one byte for declaring a type of itself and data validity, and the + * subsequent two bytes for input data of a DTVCC packet. There are 4 types for cc_data packet. + * We're interested in DTVCC_PACKET_START(type 3) and DTVCC_PACKET_DATA(type 2). Each DTVCC packet + * begins with DTVCC_PACKET_START(type 3) and the following cc_data packets which has + * DTVCC_PACKET_DATA(type 2) are appended into the DTVCC packet being assembled. + * + * <h3>Step 3. DTVCC packet -> Service Blocks ({@link #parseDtvCcPacket} method)</h3> + * + * <p>A DTVCC packet consists of multiple service blocks. Each service block represents a caption + * track and has a service number, which ranges from 1 to 63, that denotes caption track identity. + * In here, we listen at most one chosen caption track by {@link #mListenServiceNumber}. Otherwise, + * just skip the other service blocks. + * + * <h3>Step 4. Interpreting Service Block Data ({@link #parseServiceBlockData}, {@code parseXX}, and + * {@link #parseExt1} methods)</h3> + * + * <p>Service block data is actual caption stream. it looks similar to telnet. It uses most parts of + * ASCII table and consists of specially defined commands and some ASCII control codes which work in + * a behavior slightly different from their original purpose. ASCII control codes and caption + * commands are explicit instructions that control the state of a closed caption service and the + * other ASCII and text codes are implicit instructions that send their characters to buffer. + * + * <p>There are 4 main code groups and 4 extended code groups. Both the range of code groups are the + * same as the range of a byte. + * + * <p>4 main code groups: C0, C1, G0, G1 <br> + * 4 extended code groups: C2, C3, G2, G3 + * + * <p>Each code group has its own handle method. For example, {@link #parseC0} handles C0 code group + * and so on. And {@link #parseServiceBlockData} method maps a stream on the main code groups while + * {@link #parseExt1} method maps on the extended code groups. + * + * <p>The main code groups: + * + * <ul> + * <li>C0 - contains modified ASCII control codes. It is not intended by CEA-708 but Korea TTA + * standard for ATSC CC uses P16 character heavily, which is unclear entity in CEA-708 doc, + * even for the alphanumeric characters instead of ASCII characters. + * <li>C1 - contains the caption commands. There are 3 categories of a caption command. + * <ul> + * <li>Window commands: The window commands control a caption window which is addressable + * area being with in the Safe title area. (CWX, CLW, DSW, HDW, TGW, DLW, SWA, DFX) + * <li>Pen commands: Th pen commands control text style and location. (SPA, SPC, SPL) + * <li>Job commands: The job commands make a delay and recover from the delay. (DLY, DLC, + * RST) + * </ul> + * <li>G0 - same as printable ASCII character set except music note character. + * <li>G1 - same as ISO 8859-1 Latin 1 character set. + * </ul> + * + * <p>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<CcPacket> 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. + * + * <p>{@link CaptionEvent#type} determines the type of the result and {@link CaptionEvent#obj} + * contains the output value of a caption event. The observer must do the casting to the + * corresponding type. + * + * <ul> + * <li>{@code CAPTION_EMIT_TYPE_BUFFER}: Passes a caption text buffer to a observer. {@code + * obj} must be of {@link String}. + * <li>{@code CAPTION_EMIT_TYPE_CONTROL}: Passes a caption character control code to a + * observer. {@code obj} must be of {@link Character}. + * <li>{@code CAPTION_EMIT_TYPE_CLEAR_COMMAND}: Passes a clear command to a observer. {@code + * obj} must be {@code NULL}. + * </ul> + */ + @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; + } +} |