• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.exoplayer;
18 
19 import android.util.Log;
20 import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
21 import com.android.tv.tuner.data.Cea708Parser;
22 import com.google.android.exoplayer.ExoPlaybackException;
23 import com.google.android.exoplayer.MediaClock;
24 import com.google.android.exoplayer.MediaFormat;
25 import com.google.android.exoplayer.MediaFormatHolder;
26 import com.google.android.exoplayer.SampleHolder;
27 import com.google.android.exoplayer.SampleSource;
28 import com.google.android.exoplayer.TrackRenderer;
29 import com.google.android.exoplayer.util.Assertions;
30 import java.io.IOException;
31 
32 /** A {@link TrackRenderer} for CEA-708 textual subtitles. */
33 public class Cea708TextTrackRenderer extends TrackRenderer
34         implements Cea708Parser.OnCea708ParserListener {
35     private static final String TAG = "Cea708TextTrackRenderer";
36     private static final boolean DEBUG = false;
37 
38     public static final int MSG_SERVICE_NUMBER = 1;
39     public static final int MSG_ENABLE_CLOSED_CAPTION = 2;
40 
41     // According to CEA-708B, the maximum value of closed caption bandwidth is 9600bps.
42     private static final int DEFAULT_INPUT_BUFFER_SIZE = 9600 / 8;
43 
44     private final SampleSource.SampleSourceReader mSource;
45     private final SampleHolder mSampleHolder;
46     private final MediaFormatHolder mFormatHolder;
47     private int mServiceNumber;
48     private boolean mInputStreamEnded;
49     private long mCurrentPositionUs;
50     private long mPresentationTimeUs;
51     private int mTrackIndex;
52     private boolean mRenderingDisabled;
53     private Cea708Parser mCea708Parser;
54     private CcListener mCcListener;
55 
56     public interface CcListener {
emitEvent(CaptionEvent captionEvent)57         void emitEvent(CaptionEvent captionEvent);
58 
clearCaption()59         void clearCaption();
60 
discoverServiceNumber(int serviceNumber)61         void discoverServiceNumber(int serviceNumber);
62     }
63 
Cea708TextTrackRenderer(SampleSource source)64     public Cea708TextTrackRenderer(SampleSource source) {
65         mSource = source.register();
66         mTrackIndex = -1;
67         mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT);
68         mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE);
69         mFormatHolder = new MediaFormatHolder();
70     }
71 
72     @Override
getMediaClock()73     protected MediaClock getMediaClock() {
74         return null;
75     }
76 
handlesMimeType(String mimeType)77     private boolean handlesMimeType(String mimeType) {
78         return mimeType.equals(MpegTsSampleExtractor.MIMETYPE_TEXT_CEA_708);
79     }
80 
81     @Override
doPrepare(long positionUs)82     protected boolean doPrepare(long positionUs) throws ExoPlaybackException {
83         boolean sourcePrepared = mSource.prepare(positionUs);
84         if (!sourcePrepared) {
85             return false;
86         }
87         int trackCount = mSource.getTrackCount();
88         for (int i = 0; i < trackCount; ++i) {
89             MediaFormat trackFormat = mSource.getFormat(i);
90             if (handlesMimeType(trackFormat.mimeType)) {
91                 mTrackIndex = i;
92                 clearDecodeState();
93                 return true;
94             }
95         }
96         // TODO: Check this case. (Source do not have the proper mime type.)
97         return true;
98     }
99 
100     @Override
onEnabled(int track, long positionUs, boolean joining)101     protected void onEnabled(int track, long positionUs, boolean joining) {
102         Assertions.checkArgument(mTrackIndex != -1 && track == 0);
103         mSource.enable(mTrackIndex, positionUs);
104         mInputStreamEnded = false;
105         mPresentationTimeUs = positionUs;
106         mCurrentPositionUs = Long.MIN_VALUE;
107     }
108 
109     @Override
onDisabled()110     protected void onDisabled() {
111         mSource.disable(mTrackIndex);
112     }
113 
114     @Override
onReleased()115     protected void onReleased() {
116         mSource.release();
117         mCea708Parser = null;
118     }
119 
120     @Override
isEnded()121     protected boolean isEnded() {
122         return mInputStreamEnded;
123     }
124 
125     @Override
isReady()126     protected boolean isReady() {
127         // Since this track will be fed by {@link VideoTrackRenderer},
128         // it is not required to control transition between ready state and buffering state.
129         return true;
130     }
131 
132     @Override
getTrackCount()133     protected int getTrackCount() {
134         return mTrackIndex < 0 ? 0 : 1;
135     }
136 
137     @Override
getFormat(int track)138     protected MediaFormat getFormat(int track) {
139         Assertions.checkArgument(mTrackIndex != -1 && track == 0);
140         return mSource.getFormat(mTrackIndex);
141     }
142 
143     @Override
maybeThrowError()144     protected void maybeThrowError() throws ExoPlaybackException {
145         try {
146             mSource.maybeThrowError();
147         } catch (IOException e) {
148             throw new ExoPlaybackException(e);
149         }
150     }
151 
152     @Override
doSomeWork(long positionUs, long elapsedRealtimeUs)153     protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
154         try {
155             mPresentationTimeUs = positionUs;
156             if (!mInputStreamEnded) {
157                 processOutput();
158                 feedInputBuffer();
159             }
160         } catch (IOException e) {
161             throw new ExoPlaybackException(e);
162         }
163     }
164 
processOutput()165     private boolean processOutput() {
166         return !mInputStreamEnded
167                 && mCea708Parser != null
168                 && mCea708Parser.processClosedCaptions(mPresentationTimeUs);
169     }
170 
feedInputBuffer()171     private boolean feedInputBuffer() throws IOException, ExoPlaybackException {
172         if (mInputStreamEnded) {
173             return false;
174         }
175         long discontinuity = mSource.readDiscontinuity(mTrackIndex);
176         if (discontinuity != SampleSource.NO_DISCONTINUITY) {
177             if (DEBUG) {
178                 Log.d(TAG, "Read discontinuity happened");
179             }
180 
181             // TODO: handle input discontinuity for trickplay.
182             clearDecodeState();
183             mPresentationTimeUs = discontinuity;
184             return false;
185         }
186         mSampleHolder.data.clear();
187         mSampleHolder.size = 0;
188         int result =
189                 mSource.readData(mTrackIndex, mPresentationTimeUs, mFormatHolder, mSampleHolder);
190         switch (result) {
191             case SampleSource.NOTHING_READ:
192                 {
193                     return false;
194                 }
195             case SampleSource.FORMAT_READ:
196                 {
197                     if (DEBUG) {
198                         Log.i(TAG, "Format was read again");
199                     }
200                     return true;
201                 }
202             case SampleSource.END_OF_STREAM:
203                 {
204                     if (DEBUG) {
205                         Log.i(TAG, "End of stream from SampleSource");
206                     }
207                     mInputStreamEnded = true;
208                     return false;
209                 }
210             case SampleSource.SAMPLE_READ:
211                 {
212                     mSampleHolder.data.flip();
213                     if (mCea708Parser != null && !mRenderingDisabled) {
214                         mCea708Parser.parseClosedCaption(mSampleHolder.data, mSampleHolder.timeUs);
215                     }
216                     return true;
217                 }
218         }
219         return false;
220     }
221 
clearDecodeState()222     private void clearDecodeState() {
223         mCea708Parser = new Cea708Parser();
224         mCea708Parser.setListener(this);
225         mCea708Parser.setListenServiceNumber(mServiceNumber);
226     }
227 
228     @Override
getDurationUs()229     protected long getDurationUs() {
230         return mSource.getFormat(mTrackIndex).durationUs;
231     }
232 
233     @Override
getBufferedPositionUs()234     protected long getBufferedPositionUs() {
235         return mSource.getBufferedPositionUs();
236     }
237 
238     @Override
seekTo(long currentPositionUs)239     protected void seekTo(long currentPositionUs) throws ExoPlaybackException {
240         mSource.seekToUs(currentPositionUs);
241         mInputStreamEnded = false;
242         mPresentationTimeUs = currentPositionUs;
243         mCurrentPositionUs = Long.MIN_VALUE;
244     }
245 
246     @Override
onStarted()247     protected void onStarted() {
248         // do nothing.
249     }
250 
251     @Override
onStopped()252     protected void onStopped() {
253         // do nothing.
254     }
255 
setServiceNumber(int serviceNumber)256     private void setServiceNumber(int serviceNumber) {
257         mServiceNumber = serviceNumber;
258         if (mCea708Parser != null) {
259             mCea708Parser.setListenServiceNumber(serviceNumber);
260         }
261     }
262 
263     @Override
emitEvent(CaptionEvent event)264     public void emitEvent(CaptionEvent event) {
265         if (mCcListener != null) {
266             mCcListener.emitEvent(event);
267         }
268     }
269 
270     @Override
discoverServiceNumber(int serviceNumber)271     public void discoverServiceNumber(int serviceNumber) {
272         if (mCcListener != null) {
273             mCcListener.discoverServiceNumber(serviceNumber);
274         }
275     }
276 
setCcListener(CcListener ccListener)277     public void setCcListener(CcListener ccListener) {
278         mCcListener = ccListener;
279     }
280 
281     @Override
handleMessage(int messageType, Object message)282     public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
283         switch (messageType) {
284             case MSG_SERVICE_NUMBER:
285                 setServiceNumber((int) message);
286                 break;
287             case MSG_ENABLE_CLOSED_CAPTION:
288                 boolean renderingDisabled = (Boolean) message == false;
289                 if (mRenderingDisabled != renderingDisabled) {
290                     mRenderingDisabled = renderingDisabled;
291                     if (mRenderingDisabled) {
292                         if (mCea708Parser != null) {
293                             mCea708Parser.clear();
294                         }
295                         if (mCcListener != null) {
296                             mCcListener.clearCaption();
297                         }
298                     }
299                 }
300                 break;
301             default:
302                 super.handleMessage(messageType, message);
303         }
304     }
305 }
306