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 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 final SampleSource.SampleSourceReader mSource; 48 private final SampleHolder mSampleHolder; 49 private final 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(MpegTsSampleExtractor.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 return !mInputStreamEnded && mCea708Parser != null && 166 mCea708Parser.processClosedCaptions(mPresentationTimeUs); 167 } 168 feedInputBuffer()169 private boolean feedInputBuffer() throws IOException, ExoPlaybackException { 170 if (mInputStreamEnded) { 171 return false; 172 } 173 long discontinuity = mSource.readDiscontinuity(mTrackIndex); 174 if (discontinuity != SampleSource.NO_DISCONTINUITY) { 175 if (DEBUG) { 176 Log.d(TAG, "Read discontinuity happened"); 177 } 178 179 // TODO: handle input discontinuity for trickplay. 180 clearDecodeState(); 181 mPresentationTimeUs = discontinuity; 182 return false; 183 } 184 mSampleHolder.data.clear(); 185 mSampleHolder.size = 0; 186 int result = mSource.readData(mTrackIndex, mPresentationTimeUs, 187 mFormatHolder, mSampleHolder); 188 switch (result) { 189 case SampleSource.NOTHING_READ: { 190 return false; 191 } 192 case SampleSource.FORMAT_READ: { 193 if (DEBUG) { 194 Log.i(TAG, "Format was read again"); 195 } 196 return true; 197 } 198 case SampleSource.END_OF_STREAM: { 199 if (DEBUG) { 200 Log.i(TAG, "End of stream from SampleSource"); 201 } 202 mInputStreamEnded = true; 203 return false; 204 } 205 case SampleSource.SAMPLE_READ: { 206 mSampleHolder.data.flip(); 207 if (mCea708Parser != null) { 208 mCea708Parser.parseClosedCaption(mSampleHolder.data, mSampleHolder.timeUs); 209 } 210 return true; 211 } 212 } 213 return false; 214 } 215 clearDecodeState()216 private void clearDecodeState() { 217 mCea708Parser = new Cea708Parser(); 218 mCea708Parser.setListener(this); 219 mCea708Parser.setListenServiceNumber(mServiceNumber); 220 } 221 222 @Override getDurationUs()223 protected long getDurationUs() { 224 return mSource.getFormat(mTrackIndex).durationUs; 225 } 226 227 @Override getBufferedPositionUs()228 protected long getBufferedPositionUs() { 229 return mSource.getBufferedPositionUs(); 230 } 231 232 @Override seekTo(long currentPositionUs)233 protected void seekTo(long currentPositionUs) throws ExoPlaybackException { 234 mSource.seekToUs(currentPositionUs); 235 mInputStreamEnded = false; 236 mPresentationTimeUs = currentPositionUs; 237 mCurrentPositionUs = Long.MIN_VALUE; 238 } 239 240 @Override onStarted()241 protected void onStarted() { 242 // do nothing. 243 } 244 245 @Override onStopped()246 protected void onStopped() { 247 // do nothing. 248 } 249 setServiceNumber(int serviceNumber)250 private void setServiceNumber(int serviceNumber) { 251 mServiceNumber = serviceNumber; 252 if (mCea708Parser != null) { 253 mCea708Parser.setListenServiceNumber(serviceNumber); 254 } 255 } 256 257 @Override emitEvent(CaptionEvent event)258 public void emitEvent(CaptionEvent event) { 259 if (mCcListener != null) { 260 mCcListener.emitEvent(event); 261 } 262 } 263 264 @Override discoverServiceNumber(int serviceNumber)265 public void discoverServiceNumber(int serviceNumber) { 266 if (mCcListener != null) { 267 mCcListener.discoverServiceNumber(serviceNumber); 268 } 269 } 270 setCcListener(CcListener ccListener)271 public void setCcListener(CcListener ccListener) { 272 mCcListener = ccListener; 273 } 274 275 @Override handleMessage(int messageType, Object message)276 public void handleMessage(int messageType, Object message) throws ExoPlaybackException { 277 if (messageType == MSG_SERVICE_NUMBER) { 278 setServiceNumber((int) message); 279 } else { 280 super.handleMessage(messageType, message); 281 } 282 } 283 } 284