blob: a391db504ffc16e81abe84e125eedad63288a339 [file] [log] [blame]
/*
* 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.usbtuner.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.usbtuner.cc.Cea708Parser;
import com.android.usbtuner.cc.Cea708Parser.OnCea708ParserListener;
import com.android.usbtuner.data.Cea708Data.CaptionEvent;
import java.io.IOException;
/**
* A {@link TrackRenderer} for CEA-708 textual subtitles.
*/
public class Cea708TextTrackRenderer extends TrackRenderer implements 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 SampleSource.SampleSourceReader mSource;
private SampleHolder mSampleHolder;
private 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(MpegTsSampleSourceExtractor.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() {
if (mInputStreamEnded) {
return false;
}
return 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);
}
}
}