/* * 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.nano.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 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 getWindowsFromBitmap(int windowBitmap) { ArrayList 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); } } }