| /* |
| * 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.Handler; |
| import android.os.Message; |
| import android.util.Log; |
| import android.view.View; |
| |
| 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.Track.AtscCaptionTrack; |
| |
| import java.util.ArrayList; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * Decodes and renders CEA-708. |
| */ |
| public class CaptionTrackRenderer 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 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 = "CaptionTrackRenderer"; |
| private static final boolean DEBUG = false; |
| |
| private static final long DELAY_IN_MILLIS = TimeUnit.MILLISECONDS.toMillis(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 CaptionLayout mCaptionLayout; |
| private boolean mIsDelayed = false; |
| private CaptionWindowLayout mCurrentWindowLayout; |
| private final CaptionWindowLayout[] mCaptionWindowLayouts = |
| new CaptionWindowLayout[CAPTION_WINDOWS_MAX]; |
| private final ArrayList<CaptionEvent> mPendingCaptionEvents = new ArrayList<>(); |
| private final Handler mHandler; |
| |
| public CaptionTrackRenderer(CaptionLayout captionLayout) { |
| mCaptionLayout = captionLayout; |
| 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 start(AtscCaptionTrack captionTrack) { |
| if (captionTrack == null) { |
| stop(); |
| return; |
| } |
| if (DEBUG) { |
| Log.d(TAG, "Start captionTrack " + captionTrack.language); |
| } |
| reset(); |
| mCaptionLayout.setCaptionTrack(captionTrack); |
| mCaptionLayout.setVisibility(View.VISIBLE); |
| } |
| |
| public void stop() { |
| if (DEBUG) { |
| Log.d(TAG, "Stop captionTrack"); |
| } |
| mCaptionLayout.setVisibility(View.INVISIBLE); |
| mHandler.removeMessages(MSG_CAPTION_CLEAR); |
| } |
| |
| public void processCaptionEvent(CaptionEvent event) { |
| if (mIsDelayed) { |
| mPendingCaptionEvents.add(event); |
| return; |
| } |
| switch (event.type) { |
| case Cea708Parser.CAPTION_EMIT_TYPE_BUFFER: |
| sendBufferToCurrentWindow((String) event.obj); |
| break; |
| case Cea708Parser.CAPTION_EMIT_TYPE_CONTROL: |
| sendControlToCurrentWindow((char) event.obj); |
| break; |
| case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_CWX: |
| setCurrentWindowLayout((int) event.obj); |
| break; |
| case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_CLW: |
| clearWindows((int) event.obj); |
| break; |
| case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DSW: |
| displayWindows((int) event.obj); |
| break; |
| case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_HDW: |
| hideWindows((int) event.obj); |
| break; |
| case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_TGW: |
| toggleWindows((int) event.obj); |
| break; |
| case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DLW: |
| deleteWindows((int) event.obj); |
| break; |
| case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DLY: |
| delay((int) event.obj); |
| break; |
| case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DLC: |
| delayCancel(); |
| break; |
| case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_RST: |
| reset(); |
| break; |
| case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SPA: |
| setPenAttr((CaptionPenAttr) event.obj); |
| break; |
| case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SPC: |
| setPenColor((CaptionPenColor) event.obj); |
| break; |
| case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SPL: |
| setPenLocation((CaptionPenLocation) event.obj); |
| break; |
| case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SWA: |
| setWindowAttr((CaptionWindowAttr) event.obj); |
| break; |
| case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DFX: |
| defineWindow((CaptionWindow) event.obj); |
| break; |
| } |
| } |
| |
| // The window related caption commands |
| private void setCurrentWindowLayout(int windowId) { |
| if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) { |
| return; |
| } |
| CaptionWindowLayout 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<CaptionWindowLayout> getWindowsFromBitmap(int windowBitmap) { |
| ArrayList<CaptionWindowLayout> windows = new ArrayList<>(); |
| for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) { |
| if ((windowBitmap & (1 << i)) != 0) { |
| CaptionWindowLayout windowLayout = mCaptionWindowLayouts[i]; |
| if (windowLayout != null) { |
| windows.add(windowLayout); |
| } |
| } |
| } |
| return windows; |
| } |
| |
| private void clearWindows(int windowBitmap) { |
| if (windowBitmap == 0) { |
| return; |
| } |
| for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { |
| windowLayout.clear(); |
| } |
| } |
| |
| private void displayWindows(int windowBitmap) { |
| if (windowBitmap == 0) { |
| return; |
| } |
| for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { |
| windowLayout.show(); |
| } |
| } |
| |
| private void hideWindows(int windowBitmap) { |
| if (windowBitmap == 0) { |
| return; |
| } |
| for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { |
| windowLayout.hide(); |
| } |
| } |
| |
| private void toggleWindows(int windowBitmap) { |
| if (windowBitmap == 0) { |
| return; |
| } |
| for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { |
| if (windowLayout.isShown()) { |
| windowLayout.hide(); |
| } else { |
| windowLayout.show(); |
| } |
| } |
| } |
| |
| private void deleteWindows(int windowBitmap) { |
| if (windowBitmap == 0) { |
| return; |
| } |
| for (CaptionWindowLayout 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; |
| } |
| mCaptionLayout.setVisibility(View.INVISIBLE); |
| mHandler.removeMessages(MSG_CAPTION_CLEAR); |
| } |
| |
| private void setWindowAttr(CaptionWindowAttr windowAttr) { |
| if (mCurrentWindowLayout != null) { |
| mCurrentWindowLayout.setWindowAttr(windowAttr); |
| } |
| } |
| |
| private void defineWindow(CaptionWindow window) { |
| if (window == null) { |
| return; |
| } |
| int windowId = window.id; |
| if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) { |
| return; |
| } |
| CaptionWindowLayout windowLayout = mCaptionWindowLayouts[windowId]; |
| if (windowLayout == null) { |
| windowLayout = new CaptionWindowLayout(mCaptionLayout.getContext()); |
| } |
| windowLayout.initWindow(mCaptionLayout, 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 * DELAY_IN_MILLIS); |
| } |
| |
| private void delayCancel() { |
| mIsDelayed = false; |
| processPendingBuffer(); |
| } |
| |
| private void processPendingBuffer() { |
| for (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(CaptionPenAttr attr) { |
| if (mCurrentWindowLayout != null) { |
| mCurrentWindowLayout.setPenAttr(attr); |
| } |
| } |
| |
| private void setPenColor(CaptionPenColor color) { |
| if (mCurrentWindowLayout != null) { |
| mCurrentWindowLayout.setPenColor(color); |
| } |
| } |
| |
| private void setPenLocation(CaptionPenLocation location) { |
| if (mCurrentWindowLayout != null) { |
| mCurrentWindowLayout.setPenLocation(location.row, location.column); |
| } |
| } |
| } |