/*
* Copyright (C) 2014 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.google.android.exoplayer.text;
import com.google.android.exoplayer.ExoPlaybackException;
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.SampleSourceTrackRenderer;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.util.Assertions;
import android.annotation.TargetApi;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A {@link TrackRenderer} for subtitles. Text is parsed from sample data using a
* {@link SubtitleParser}. The actual rendering of each line of text is delegated to a
* {@link TextRenderer}.
* <p>
* If no {@link SubtitleParser} instances are passed to the constructor, the subtitle type will be
* detected automatically for the following supported formats:
*
* <ul>
* <li>WebVTT ({@link com.google.android.exoplayer.text.webvtt.WebvttParser})</li>
* <li>TTML
* ({@link com.google.android.exoplayer.text.ttml.TtmlParser})</li>
* <li>SubRip
* ({@link com.google.android.exoplayer.text.subrip.SubripParser})</li>
* <li>TX3G
* ({@link com.google.android.exoplayer.text.tx3g.Tx3gParser})</li>
* </ul>
*
* <p>To override the default parsers, pass one or more {@link SubtitleParser} instances to the
* constructor. The first {@link SubtitleParser} that returns {@code true} from
* {@link SubtitleParser#canParse(String)} will be used.
*/
@TargetApi(16)
public final class TextTrackRenderer extends SampleSourceTrackRenderer implements Callback {
private static final int MSG_UPDATE_OVERLAY = 0;
/**
* Default parser classes in priority order. They are referred to indirectly so that it is
* possible to remove unused parsers.
*/
private static final List<Class<? extends SubtitleParser>> DEFAULT_PARSER_CLASSES;
static {
DEFAULT_PARSER_CLASSES = new ArrayList<>();
// Load parsers using reflection so that they can be deleted cleanly.
// Class.forName(<class name>) appears for each parser so that automated tools like proguard
// can detect the use of reflection (see http://proguard.sourceforge.net/FAQ.html#forname).
try {
DEFAULT_PARSER_CLASSES.add(
Class.forName("com.google.android.exoplayer.text.webvtt.WebvttParser")
.asSubclass(SubtitleParser.class));
} catch (ClassNotFoundException e) {
// Parser not found.
}
try {
DEFAULT_PARSER_CLASSES.add(
Class.forName("com.google.android.exoplayer.text.ttml.TtmlParser")
.asSubclass(SubtitleParser.class));
} catch (ClassNotFoundException e) {
// Parser not found.
}
try {
DEFAULT_PARSER_CLASSES.add(
Class.forName("com.google.android.exoplayer.text.subrip.SubripParser")
.asSubclass(SubtitleParser.class));
} catch (ClassNotFoundException e) {
// Parser not found.
}
try {
DEFAULT_PARSER_CLASSES.add(
Class.forName("com.google.android.exoplayer.text.tx3g.Tx3gParser")
.asSubclass(SubtitleParser.class));
} catch (ClassNotFoundException e) {
// Parser not found.
}
}
private final Handler textRendererHandler;
private final TextRenderer textRenderer;
private final MediaFormatHolder formatHolder;
private final SubtitleParser[] subtitleParsers;
private int parserIndex;
private boolean inputStreamEnded;
private PlayableSubtitle subtitle;
private PlayableSubtitle nextSubtitle;
private SubtitleParserHelper parserHelper;
private HandlerThread parserThread;
private int nextSubtitleEventIndex;
/**
* @param source A source from which samples containing subtitle data can be read.
* @param textRenderer The text renderer.
* @param textRendererLooper The looper associated with the thread on which textRenderer should be
* invoked. If the renderer makes use of standard Android UI components, then this should
* normally be the looper associated with the applications' main thread, which can be
* obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the
* renderer should be invoked directly on the player's internal rendering thread.
* @param subtitleParsers {@link SubtitleParser}s to parse text samples, in order of decreasing
* priority. If omitted, the default parsers will be used.
*/
public TextTrackRenderer(SampleSource source, TextRenderer textRenderer,
Looper textRendererLooper, SubtitleParser... subtitleParsers) {
this(new SampleSource[] {source}, textRenderer, textRendererLooper, subtitleParsers);
}
/**
* @param sources Sources from which samples containing subtitle data can be read.
* @param textRenderer The text renderer.
* @param textRendererLooper The looper associated with the thread on which textRenderer should be
* invoked. If the renderer makes use of standard Android UI components, then this should
* normally be the looper associated with the applications' main thread, which can be
* obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the
* renderer should be invoked directly on the player's internal rendering thread.
* @param subtitleParsers {@link SubtitleParser}s to parse text samples, in order of decreasing
* priority. If omitted, the default parsers will be used.
*/
public TextTrackRenderer(SampleSource[] sources, TextRenderer textRenderer,
Looper textRendererLooper, SubtitleParser... subtitleParsers) {
super(sources);
this.textRenderer = Assertions.checkNotNull(textRenderer);
this.textRendererHandler = textRendererLooper == null ? null
: new Handler(textRendererLooper, this);
if (subtitleParsers == null || subtitleParsers.length == 0) {
subtitleParsers = new SubtitleParser[DEFAULT_PARSER_CLASSES.size()];
for (int i = 0; i < subtitleParsers.length; i++) {
try {
subtitleParsers[i] = DEFAULT_PARSER_CLASSES.get(i).newInstance();
} catch (InstantiationException e) {
throw new IllegalStateException("Unexpected error creating default parser", e);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Unexpected error creating default parser", e);
}
}
}
this.subtitleParsers = subtitleParsers;
formatHolder = new MediaFormatHolder();
}
@Override
protected boolean handlesTrack(MediaFormat mediaFormat) {
return getParserIndex(mediaFormat) != -1;
}
@Override
protected void onEnabled(int track, long positionUs, boolean joining)
throws ExoPlaybackException {
super.onEnabled(track, positionUs, joining);
parserIndex = getParserIndex(getFormat(track));
parserThread = new HandlerThread("textParser");
parserThread.start();
parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParsers[parserIndex]);
seekToInternal();
}
@Override
protected void seekTo(long positionUs) throws ExoPlaybackException {
super.seekTo(positionUs);
seekToInternal();
}
private void seekToInternal() {
inputStreamEnded = false;
subtitle = null;
nextSubtitle = null;
parserHelper.flush();
clearTextRenderer();
}
@Override
protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
continueBufferingSource(positionUs);
if (nextSubtitle == null) {
try {
nextSubtitle = parserHelper.getAndClearResult();
} catch (IOException e) {
throw new ExoPlaybackException(e);
}
}
if (getState() != TrackRenderer.STATE_STARTED) {
return;
}
boolean textRendererNeedsUpdate = false;
long subtitleNextEventTimeUs = Long.MAX_VALUE;
if (subtitle != null) {
// We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we
// advance to the next event.
subtitleNextEventTimeUs = getNextEventTime();
while (subtitleNextEventTimeUs <= positionUs) {
nextSubtitleEventIndex++;
subtitleNextEventTimeUs = getNextEventTime();
textRendererNeedsUpdate = true;
}
}
if (nextSubtitle != null && nextSubtitle.startTimeUs <= positionUs) {
// Advance to the next subtitle. Sync the next event index and trigger an update.
subtitle = nextSubtitle;
nextSubtitle = null;
nextSubtitleEventIndex = subtitle.getNextEventTimeIndex(positionUs);
textRendererNeedsUpdate = true;
}
if (textRendererNeedsUpdate) {
// textRendererNeedsUpdate is set and we're playing. Update the renderer.
updateTextRenderer(subtitle.getCues(positionUs));
}
if (!inputStreamEnded && nextSubtitle == null && !parserHelper.isParsing()) {
// Try and read the next subtitle from the source.
SampleHolder sampleHolder = parserHelper.getSampleHolder();
sampleHolder.clearData();
int result = readSource(positionUs, formatHolder, sampleHolder, false);
if (result == SampleSource.FORMAT_READ) {
parserHelper.setFormat(formatHolder.format);
} else if (result == SampleSource.SAMPLE_READ) {
parserHelper.startParseOperation();
} else if (result == SampleSource.END_OF_STREAM) {
inputStreamEnded = true;
}
}
}
@Override
protected void onDisabled() throws ExoPlaybackException {
subtitle = null;
nextSubtitle = null;
parserThread.quit();
parserThread = null;
parserHelper = null;
clearTextRenderer();
super.onDisabled();
}
@Override
protected long getBufferedPositionUs() {
// Don't block playback whilst subtitles are loading.
return END_OF_TRACK_US;
}
@Override
protected boolean isEnded() {
return inputStreamEnded && (subtitle == null || getNextEventTime() == Long.MAX_VALUE);
}
@Override
protected boolean isReady() {
// Don't block playback whilst subtitles are loading.
// Note: To change this behavior, it will be necessary to consider [Internal: b/12949941].
return true;
}
private long getNextEventTime() {
return ((nextSubtitleEventIndex == -1)
|| (nextSubtitleEventIndex >= subtitle.getEventTimeCount())) ? Long.MAX_VALUE
: (subtitle.getEventTime(nextSubtitleEventIndex));
}
private void updateTextRenderer(List<Cue> cues) {
if (textRendererHandler != null) {
textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, cues).sendToTarget();
} else {
invokeRendererInternalCues(cues);
}
}
private void clearTextRenderer() {
updateTextRenderer(Collections.<Cue>emptyList());
}
@SuppressWarnings("unchecked")
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_UPDATE_OVERLAY:
invokeRendererInternalCues((List<Cue>) msg.obj);
return true;
}
return false;
}
private void invokeRendererInternalCues(List<Cue> cues) {
textRenderer.onCues(cues);
}
private int getParserIndex(MediaFormat mediaFormat) {
for (int i = 0; i < subtitleParsers.length; i++) {
if (subtitleParsers[i].canParse(mediaFormat.mimeType)) {
return i;
}
}
return -1;
}
}