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