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