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