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