1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tv.tuner.cc; 18 19 import android.os.Handler; 20 import android.os.Message; 21 import android.util.Log; 22 import android.view.View; 23 24 import com.android.tv.common.flags.TunerFlags; 25 import com.android.tv.tuner.data.Cea708Data.CaptionEvent; 26 import com.android.tv.tuner.data.Cea708Data.CaptionPenAttr; 27 import com.android.tv.tuner.data.Cea708Data.CaptionPenColor; 28 import com.android.tv.tuner.data.Cea708Data.CaptionPenLocation; 29 import com.android.tv.tuner.data.Cea708Data.CaptionWindow; 30 import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr; 31 import com.android.tv.tuner.data.Cea708Parser; 32 import com.android.tv.tuner.data.Track.AtscCaptionTrack; 33 import com.google.android.exoplayer2.text.Cue; 34 import com.google.auto.factory.AutoFactory; 35 import com.google.auto.factory.Provided; 36 37 import java.util.ArrayList; 38 import java.util.List; 39 40 /** Decodes and renders CEA-708. */ 41 public class CaptionTrackRenderer implements Handler.Callback { 42 // TODO: Remaining works 43 // CaptionTrackRenderer does not support the full spec of CEA-708. The remaining works are 44 // described in the follows. 45 // C0 Table: Backspace, FF, and HCR are not supported. The rule for P16 is not standardized but 46 // it is handled as EUC-KR charset for korea broadcasting. 47 // C1 Table: All styles of windows and pens except underline, italic, pen size, and pen offset 48 // specified in CEA-708 are ignored and this follows system wide cc preferences for 49 // look and feel. SetPenLocation is not implemented. 50 // G2 Table: TSP, NBTSP and BLK are not supported. 51 // Text/commands: Word wrapping, fonts, row and column locking are not supported. 52 53 private static final String TAG = "CaptionTrackRenderer"; 54 private static final boolean DEBUG = false; 55 56 private static final long DELAY_IN_MILLIS = 100 /* milliseconds */; 57 58 // According to CEA-708B, there can exist up to 8 caption windows. 59 private static final int CAPTION_WINDOWS_MAX = 8; 60 private static final int CAPTION_ALL_WINDOWS_BITMAP = 255; 61 62 private static final int MSG_DELAY_CANCEL = 1; 63 private static final int MSG_CAPTION_CLEAR = 2; 64 65 private static final long CAPTION_CLEAR_INTERVAL_MS = 60000; 66 67 private final CaptionLayout mCaptionLayout; 68 private final CaptionWindowLayout.Factory mCaptionWindowLayoutFactory; 69 private final TunerFlags mTunerFlags; 70 private boolean mIsDelayed = false; 71 private CaptionWindowLayout mCurrentWindowLayout; 72 private final CaptionWindowLayout[] mCaptionWindowLayouts = 73 new CaptionWindowLayout[CAPTION_WINDOWS_MAX]; 74 private final ArrayList<CaptionEvent> mPendingCaptionEvents = new ArrayList<>(); 75 private final Handler mHandler; 76 77 /** 78 * Factory for {@link CaptionTrackRenderer}. 79 * 80 * <p>This wrapper class keeps other classes from needing to reference the {@link AutoFactory} 81 * generated class. 82 */ 83 public interface Factory { create(CaptionLayout captionLayout)84 public CaptionTrackRenderer create(CaptionLayout captionLayout); 85 } 86 87 @AutoFactory(implementing = Factory.class) CaptionTrackRenderer( CaptionLayout captionLayout, @Provided CaptionWindowLayout.Factory captionWindowLayoutFactory, @Provided TunerFlags tunerFlags)88 public CaptionTrackRenderer( 89 CaptionLayout captionLayout, 90 @Provided CaptionWindowLayout.Factory captionWindowLayoutFactory, 91 @Provided TunerFlags tunerFlags) { 92 mCaptionLayout = captionLayout; 93 mHandler = new Handler(this); 94 mCaptionWindowLayoutFactory = captionWindowLayoutFactory; 95 mTunerFlags = tunerFlags; 96 } 97 98 @Override handleMessage(Message msg)99 public boolean handleMessage(Message msg) { 100 switch (msg.what) { 101 case MSG_DELAY_CANCEL: 102 delayCancel(); 103 return true; 104 case MSG_CAPTION_CLEAR: 105 clearWindows(CAPTION_ALL_WINDOWS_BITMAP); 106 return true; 107 } 108 return false; 109 } 110 start(AtscCaptionTrack captionTrack)111 public void start(AtscCaptionTrack captionTrack) { 112 if (captionTrack == null) { 113 stop(); 114 return; 115 } 116 if (DEBUG) { 117 Log.d(TAG, "Start captionTrack " + captionTrack.getLanguage()); 118 } 119 reset(); 120 mCaptionLayout.setCaptionTrack(captionTrack); 121 mCaptionLayout.setVisibility(View.VISIBLE); 122 } 123 stop()124 public void stop() { 125 if (DEBUG) { 126 Log.d(TAG, "Stop captionTrack"); 127 } 128 mCaptionLayout.setVisibility(View.INVISIBLE); 129 mHandler.removeMessages(MSG_CAPTION_CLEAR); 130 } 131 processCaptionEvent(CaptionEvent event)132 public void processCaptionEvent(CaptionEvent event) { 133 if (mIsDelayed) { 134 mPendingCaptionEvents.add(event); 135 return; 136 } 137 switch (event.type) { 138 case Cea708Parser.CAPTION_EMIT_TYPE_BUFFER: 139 if (mTunerFlags.useExoplayerV2()) { 140 sendCuesToCurrentWindow((List<Cue>) event.obj); 141 } else { 142 sendBufferToCurrentWindow((String) event.obj); 143 } 144 break; 145 case Cea708Parser.CAPTION_EMIT_TYPE_CONTROL: 146 sendControlToCurrentWindow((char) event.obj); 147 break; 148 case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_CWX: 149 setCurrentWindowLayout((int) event.obj); 150 break; 151 case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_CLW: 152 clearWindows((int) event.obj); 153 break; 154 case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DSW: 155 displayWindows((int) event.obj); 156 break; 157 case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_HDW: 158 hideWindows((int) event.obj); 159 break; 160 case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_TGW: 161 toggleWindows((int) event.obj); 162 break; 163 case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DLW: 164 deleteWindows((int) event.obj); 165 break; 166 case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DLY: 167 delay((int) event.obj); 168 break; 169 case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DLC: 170 delayCancel(); 171 break; 172 case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_RST: 173 reset(); 174 break; 175 case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SPA: 176 setPenAttr((CaptionPenAttr) event.obj); 177 break; 178 case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SPC: 179 setPenColor((CaptionPenColor) event.obj); 180 break; 181 case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SPL: 182 setPenLocation((CaptionPenLocation) event.obj); 183 break; 184 case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SWA: 185 setWindowAttr((CaptionWindowAttr) event.obj); 186 break; 187 case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DFX: 188 defineWindow((CaptionWindow) event.obj); 189 break; 190 } 191 } 192 193 // The window related caption commands setCurrentWindowLayout(int windowId)194 private void setCurrentWindowLayout(int windowId) { 195 if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) { 196 return; 197 } 198 CaptionWindowLayout windowLayout = mCaptionWindowLayouts[windowId]; 199 if (windowLayout == null) { 200 return; 201 } 202 if (DEBUG) { 203 Log.d(TAG, "setCurrentWindowLayout to " + windowId); 204 } 205 mCurrentWindowLayout = windowLayout; 206 } 207 208 // Each bit of windowBitmap indicates a window. 209 // If a bit is set, the window id is the same as the number of the trailing zeros of the bit. getWindowsFromBitmap(int windowBitmap)210 private ArrayList<CaptionWindowLayout> getWindowsFromBitmap(int windowBitmap) { 211 ArrayList<CaptionWindowLayout> windows = new ArrayList<>(); 212 for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) { 213 if ((windowBitmap & (1 << i)) != 0) { 214 CaptionWindowLayout windowLayout = mCaptionWindowLayouts[i]; 215 if (windowLayout != null) { 216 windows.add(windowLayout); 217 } 218 } 219 } 220 return windows; 221 } 222 clearWindows(int windowBitmap)223 private void clearWindows(int windowBitmap) { 224 if (windowBitmap == 0) { 225 return; 226 } 227 for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { 228 windowLayout.clear(); 229 } 230 } 231 displayWindows(int windowBitmap)232 private void displayWindows(int windowBitmap) { 233 if (windowBitmap == 0) { 234 return; 235 } 236 for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { 237 windowLayout.show(); 238 } 239 } 240 hideWindows(int windowBitmap)241 private void hideWindows(int windowBitmap) { 242 if (windowBitmap == 0) { 243 return; 244 } 245 for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { 246 windowLayout.hide(); 247 } 248 } 249 toggleWindows(int windowBitmap)250 private void toggleWindows(int windowBitmap) { 251 if (windowBitmap == 0) { 252 return; 253 } 254 for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { 255 if (windowLayout.isShown()) { 256 windowLayout.hide(); 257 } else { 258 windowLayout.show(); 259 } 260 } 261 } 262 deleteWindows(int windowBitmap)263 private void deleteWindows(int windowBitmap) { 264 if (windowBitmap == 0) { 265 return; 266 } 267 for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { 268 windowLayout.removeFromCaptionView(); 269 mCaptionWindowLayouts[windowLayout.getCaptionWindowId()] = null; 270 } 271 } 272 clear()273 public void clear() { 274 mHandler.sendEmptyMessage(MSG_CAPTION_CLEAR); 275 } 276 reset()277 public void reset() { 278 mCurrentWindowLayout = null; 279 mIsDelayed = false; 280 mPendingCaptionEvents.clear(); 281 for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) { 282 if (mCaptionWindowLayouts[i] != null) { 283 mCaptionWindowLayouts[i].removeFromCaptionView(); 284 } 285 mCaptionWindowLayouts[i] = null; 286 } 287 mCaptionLayout.setVisibility(View.INVISIBLE); 288 mHandler.removeMessages(MSG_CAPTION_CLEAR); 289 } 290 setWindowAttr(CaptionWindowAttr windowAttr)291 private void setWindowAttr(CaptionWindowAttr windowAttr) { 292 if (mCurrentWindowLayout != null) { 293 mCurrentWindowLayout.setWindowAttr(windowAttr); 294 } 295 } 296 defineWindow(CaptionWindow window)297 private void defineWindow(CaptionWindow window) { 298 if (window == null) { 299 return; 300 } 301 int windowId = window.id; 302 if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) { 303 return; 304 } 305 CaptionWindowLayout windowLayout = mCaptionWindowLayouts[windowId]; 306 if (windowLayout == null) { 307 windowLayout = mCaptionWindowLayoutFactory.create(mCaptionLayout.getContext()); 308 } 309 windowLayout.initWindow(mCaptionLayout, window); 310 mCurrentWindowLayout = mCaptionWindowLayouts[windowId] = windowLayout; 311 } 312 313 // The job related caption commands delay(int tenthsOfSeconds)314 private void delay(int tenthsOfSeconds) { 315 if (tenthsOfSeconds < 0 || tenthsOfSeconds > 255) { 316 return; 317 } 318 mIsDelayed = true; 319 mHandler.sendMessageDelayed( 320 mHandler.obtainMessage(MSG_DELAY_CANCEL), tenthsOfSeconds * DELAY_IN_MILLIS); 321 } 322 delayCancel()323 private void delayCancel() { 324 mIsDelayed = false; 325 processPendingBuffer(); 326 } 327 processPendingBuffer()328 private void processPendingBuffer() { 329 for (CaptionEvent event : mPendingCaptionEvents) { 330 processCaptionEvent(event); 331 } 332 mPendingCaptionEvents.clear(); 333 } 334 335 // The implicit write caption commands sendControlToCurrentWindow(char control)336 private void sendControlToCurrentWindow(char control) { 337 if (mCurrentWindowLayout != null) { 338 mCurrentWindowLayout.sendControl(control); 339 } 340 } 341 sendCuesToCurrentWindow(List<Cue> cues)342 private void sendCuesToCurrentWindow(List<Cue> cues){ 343 if (mCurrentWindowLayout != null) { 344 mCurrentWindowLayout.setCues(cues); 345 mHandler.removeMessages(MSG_CAPTION_CLEAR); 346 mHandler.sendMessageDelayed( 347 mHandler.obtainMessage(MSG_CAPTION_CLEAR), CAPTION_CLEAR_INTERVAL_MS); 348 } 349 } 350 sendBufferToCurrentWindow(String buffer)351 private void sendBufferToCurrentWindow(String buffer) { 352 if (mCurrentWindowLayout != null) { 353 mCurrentWindowLayout.sendBuffer(buffer); 354 mHandler.removeMessages(MSG_CAPTION_CLEAR); 355 mHandler.sendMessageDelayed( 356 mHandler.obtainMessage(MSG_CAPTION_CLEAR), CAPTION_CLEAR_INTERVAL_MS); 357 } 358 } 359 360 // The pen related caption commands setPenAttr(CaptionPenAttr attr)361 private void setPenAttr(CaptionPenAttr attr) { 362 if (mCurrentWindowLayout != null) { 363 mCurrentWindowLayout.setPenAttr(attr); 364 } 365 } 366 setPenColor(CaptionPenColor color)367 private void setPenColor(CaptionPenColor color) { 368 if (mCurrentWindowLayout != null) { 369 mCurrentWindowLayout.setPenColor(color); 370 } 371 } 372 setPenLocation(CaptionPenLocation location)373 private void setPenLocation(CaptionPenLocation location) { 374 if (mCurrentWindowLayout != null) { 375 mCurrentWindowLayout.setPenLocation(location.row, location.column); 376 } 377 } 378 } 379