/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tv.tuner.exoplayer; import android.util.Log; import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.MediaClock; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.util.Assertions; import com.android.tv.tuner.cc.Cea708Parser; import com.android.tv.tuner.data.Cea708Data.CaptionEvent; import java.io.IOException; /** * A {@link TrackRenderer} for CEA-708 textual subtitles. */ public class Cea708TextTrackRenderer extends TrackRenderer implements Cea708Parser.OnCea708ParserListener { private static final String TAG = "Cea708TextTrackRenderer"; private static final boolean DEBUG = false; public static final int MSG_SERVICE_NUMBER = 1; // According to CEA-708B, the maximum value of closed caption bandwidth is 9600bps. private static final int DEFAULT_INPUT_BUFFER_SIZE = 9600 / 8; private final SampleSource.SampleSourceReader mSource; private final SampleHolder mSampleHolder; private final MediaFormatHolder mFormatHolder; private int mServiceNumber; private boolean mInputStreamEnded; private long mCurrentPositionUs; private long mPresentationTimeUs; private int mTrackIndex; private Cea708Parser mCea708Parser; private CcListener mCcListener; public interface CcListener { void emitEvent(CaptionEvent captionEvent); void discoverServiceNumber(int serviceNumber); } public Cea708TextTrackRenderer(SampleSource source) { mSource = source.register(); mTrackIndex = -1; mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT); mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE); mFormatHolder = new MediaFormatHolder(); } @Override protected MediaClock getMediaClock() { return null; } private boolean handlesMimeType(String mimeType) { return mimeType.equals(MpegTsSampleExtractor.MIMETYPE_TEXT_CEA_708); } @Override protected boolean doPrepare(long positionUs) throws ExoPlaybackException { boolean sourcePrepared = mSource.prepare(positionUs); if (!sourcePrepared) { return false; } int trackCount = mSource.getTrackCount(); for (int i = 0; i < trackCount; ++i) { MediaFormat trackFormat = mSource.getFormat(i); if (handlesMimeType(trackFormat.mimeType)) { mTrackIndex = i; clearDecodeState(); return true; } } // TODO: Check this case. (Source do not have the proper mime type.) return true; } @Override protected void onEnabled(int track, long positionUs, boolean joining) { Assertions.checkArgument(mTrackIndex != -1 && track == 0); mSource.enable(mTrackIndex, positionUs); mInputStreamEnded = false; mPresentationTimeUs = positionUs; mCurrentPositionUs = Long.MIN_VALUE; } @Override protected void onDisabled() { mSource.disable(mTrackIndex); } @Override protected void onReleased() { mSource.release(); mCea708Parser = null; } @Override protected boolean isEnded() { return mInputStreamEnded; } @Override protected boolean isReady() { // Since this track will be fed by {@link VideoTrackRenderer}, // it is not required to control transition between ready state and buffering state. return true; } @Override protected int getTrackCount() { return mTrackIndex < 0 ? 0 : 1; } @Override protected MediaFormat getFormat(int track) { Assertions.checkArgument(mTrackIndex != -1 && track == 0); return mSource.getFormat(mTrackIndex); } @Override protected void maybeThrowError() throws ExoPlaybackException { try { mSource.maybeThrowError(); } catch (IOException e) { throw new ExoPlaybackException(e); } } @Override protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { try { mPresentationTimeUs = positionUs; if (!mInputStreamEnded) { processOutput(); feedInputBuffer(); } } catch (IOException e) { throw new ExoPlaybackException(e); } } private boolean processOutput() { return !mInputStreamEnded && mCea708Parser != null && mCea708Parser.processClosedCaptions(mPresentationTimeUs); } private boolean feedInputBuffer() throws IOException, ExoPlaybackException { if (mInputStreamEnded) { return false; } long discontinuity = mSource.readDiscontinuity(mTrackIndex); if (discontinuity != SampleSource.NO_DISCONTINUITY) { if (DEBUG) { Log.d(TAG, "Read discontinuity happened"); } // TODO: handle input discontinuity for trickplay. clearDecodeState(); mPresentationTimeUs = discontinuity; return false; } mSampleHolder.data.clear(); mSampleHolder.size = 0; int result = mSource.readData(mTrackIndex, mPresentationTimeUs, mFormatHolder, mSampleHolder); switch (result) { case SampleSource.NOTHING_READ: { return false; } case SampleSource.FORMAT_READ: { if (DEBUG) { Log.i(TAG, "Format was read again"); } return true; } case SampleSource.END_OF_STREAM: { if (DEBUG) { Log.i(TAG, "End of stream from SampleSource"); } mInputStreamEnded = true; return false; } case SampleSource.SAMPLE_READ: { mSampleHolder.data.flip(); if (mCea708Parser != null) { mCea708Parser.parseClosedCaption(mSampleHolder.data, mSampleHolder.timeUs); } return true; } } return false; } private void clearDecodeState() { mCea708Parser = new Cea708Parser(); mCea708Parser.setListener(this); mCea708Parser.setListenServiceNumber(mServiceNumber); } @Override protected long getDurationUs() { return mSource.getFormat(mTrackIndex).durationUs; } @Override protected long getBufferedPositionUs() { return mSource.getBufferedPositionUs(); } @Override protected void seekTo(long currentPositionUs) throws ExoPlaybackException { mSource.seekToUs(currentPositionUs); mInputStreamEnded = false; mPresentationTimeUs = currentPositionUs; mCurrentPositionUs = Long.MIN_VALUE; } @Override protected void onStarted() { // do nothing. } @Override protected void onStopped() { // do nothing. } private void setServiceNumber(int serviceNumber) { mServiceNumber = serviceNumber; if (mCea708Parser != null) { mCea708Parser.setListenServiceNumber(serviceNumber); } } @Override public void emitEvent(CaptionEvent event) { if (mCcListener != null) { mCcListener.emitEvent(event); } } @Override public void discoverServiceNumber(int serviceNumber) { if (mCcListener != null) { mCcListener.discoverServiceNumber(serviceNumber); } } public void setCcListener(CcListener ccListener) { mCcListener = ccListener; } @Override public void handleMessage(int messageType, Object message) throws ExoPlaybackException { if (messageType == MSG_SERVICE_NUMBER) { setServiceNumber((int) message); } else { super.handleMessage(messageType, message); } } }