| /* |
| * 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.data; |
| |
| import android.os.SystemClock; |
| import android.support.annotation.IntDef; |
| import android.util.Log; |
| import android.util.SparseIntArray; |
| 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 int mDtvCcPacketCalculatedSize = 0; |
| private boolean mDtvCcPacking = false; |
| private boolean mFirstServiceNumberDiscovered; |
| |
| // Assign an empty 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; |
| mDtvCcPacketCalculatedSize = 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; |
| if (ccValid) { |
| // The dtvcc should be considered complete: |
| // if ccType is 3 or if the packet size is reached. |
| if (ccType == CC_TYPE_DTVCC_PACKET_START) { |
| if (mDtvCcPacking) { |
| parseDtvCcPacket(mDtvCcPacket.buffer(), mDtvCcPacket.length()); |
| mDtvCcPacket.clear(); |
| mDtvCcPacketCalculatedSize = 0; |
| } |
| mDtvCcPacking = true; |
| int packetSize = bytes[pos + 1] & 0x3F; // last 6 bits |
| if (packetSize == 0) { |
| packetSize = DTVCC_MAX_PACKET_SIZE; |
| } |
| mDtvCcPacketCalculatedSize = packetSize * DTVCC_PACKET_SIZE_SCALE_FACTOR; |
| 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]); |
| } |
| if ((ccType == CC_TYPE_DTVCC_PACKET_START || ccType == CC_TYPE_DTVCC_PACKET_DATA) |
| && mDtvCcPacking && mDtvCcPacket.length() == mDtvCcPacketCalculatedSize) { |
| mDtvCcPacking = false; |
| parseDtvCcPacket(mDtvCcPacket.buffer(), mDtvCcPacket.length()); |
| mDtvCcPacket.clear(); |
| mDtvCcPacketCalculatedSize = 0; |
| } |
| } |
| 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; |
| } |
| } |