/* * 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.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.dash.mpd.AdaptationSet; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.VerboseLogUtil; import android.annotation.TargetApi; import android.os.Handler; import android.os.Handler.Callback; import android.os.Looper; import android.os.Message; import android.util.Log; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; /** * A {@link TrackRenderer} for textual subtitles. The actual rendering of each line of text to a * suitable output (e.g. the display) is delegated to a {@link TextRenderer}. */ @TargetApi(16) public class TextTrackRenderer extends TrackRenderer implements Callback { /** * An interface for components that render text. */ public interface TextRenderer { /** * Invoked each time there is a change in the text to be rendered. * * @param text The text to render, or null if no text is to be rendered. */ void onText(String text); } private static final String TAG = "TextTrackRenderer"; private static final int MSG_UPDATE_OVERLAY = 0; private final Handler textRendererHandler; private final TextRenderer textRenderer; private final SampleSource source; private final SampleHolder sampleHolder; private final MediaFormatHolder formatHolder; private final SubtitleParser subtitleParser; private int trackIndex; private long currentPositionUs; private boolean inputStreamEnded; private Subtitle subtitle; private int nextSubtitleEventIndex; private boolean textRendererNeedsUpdate; /** * @param source A source from which samples containing subtitle data can be read. * @param subtitleParser A subtitle parser that will parse Subtitle objects from the source. * @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. */ public TextTrackRenderer(SampleSource source, SubtitleParser subtitleParser, TextRenderer textRenderer, Looper textRendererLooper) { this.source = Assertions.checkNotNull(source); this.subtitleParser = Assertions.checkNotNull(subtitleParser); this.textRenderer = Assertions.checkNotNull(textRenderer); this.textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper, this); formatHolder = new MediaFormatHolder(); sampleHolder = new SampleHolder(true); } @Override protected int doPrepare() throws ExoPlaybackException { try { boolean sourcePrepared = source.prepare(); if (!sourcePrepared) { return TrackRenderer.STATE_UNPREPARED; } } catch (IOException e) { throw new ExoPlaybackException(e); } for (int i = 0; i < source.getTrackCount(); i++) { if (subtitleParser.canParse(source.getTrackInfo(i).mimeType)) { trackIndex = i; return TrackRenderer.STATE_PREPARED; } } return TrackRenderer.STATE_IGNORE; } @Override protected void onEnabled(long timeUs, boolean joining) { source.enable(trackIndex, timeUs); seekToInternal(timeUs); } @Override protected void seekTo(long timeUs) { source.seekToUs(timeUs); seekToInternal(timeUs); } private void seekToInternal(long timeUs) { inputStreamEnded = false; currentPositionUs = timeUs; source.seekToUs(timeUs); if (subtitle != null && (timeUs < subtitle.getStartTime() || subtitle.getLastEventTime() <= timeUs)) { subtitle = null; } resetSampleData(); clearTextRenderer(); syncNextEventIndex(timeUs); textRendererNeedsUpdate = subtitle != null; } @Override protected void doSomeWork(long timeUs) throws ExoPlaybackException { try { source.continueBuffering(timeUs); } catch (IOException e) { throw new ExoPlaybackException(e); } currentPositionUs = timeUs; // We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we advance // to the next event. if (subtitle != null) { long nextEventTimeUs = getNextEventTime(); while (nextEventTimeUs <= timeUs) { nextSubtitleEventIndex++; nextEventTimeUs = getNextEventTime(); textRendererNeedsUpdate = true; } if (nextEventTimeUs == Long.MAX_VALUE) { // We've finished processing the subtitle. subtitle = null; } } // We don't have a subtitle. Try and read the next one from the source, and if we succeed then // sync and set textRendererNeedsUpdate. if (subtitle == null) { boolean resetSampleHolder = false; try { int result = source.readData(trackIndex, timeUs, formatHolder, sampleHolder, false); if (result == SampleSource.SAMPLE_READ) { resetSampleHolder = true; InputStream subtitleInputStream = new ByteArrayInputStream(sampleHolder.data.array(), 0, sampleHolder.size); subtitle = subtitleParser.parse(subtitleInputStream, "UTF-8", sampleHolder.timeUs); syncNextEventIndex(timeUs); textRendererNeedsUpdate = true; } else if (result == SampleSource.END_OF_STREAM) { inputStreamEnded = true; } } catch (IOException e) { resetSampleHolder = true; throw new ExoPlaybackException(e); } finally { if (resetSampleHolder) { resetSampleData(); } } } // Update the text renderer if we're both playing and textRendererNeedsUpdate is set. if (textRendererNeedsUpdate && getState() == TrackRenderer.STATE_STARTED) { textRendererNeedsUpdate = false; if (subtitle == null) { clearTextRenderer(); } else { updateTextRenderer(timeUs); } } } @Override protected void onDisabled() { source.disable(trackIndex); subtitle = null; resetSampleData(); clearTextRenderer(); } @Override protected void onReleased() { source.release(); } @Override protected long getCurrentPositionUs() { return currentPositionUs; } @Override protected long getDurationUs() { return source.getTrackInfo(trackIndex).durationUs; } @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; } @Override protected boolean isReady() { // Don't block playback whilst subtitles are loading. // Note: To change this behavior, it will be necessary to consider [redacted]. return true; } private void syncNextEventIndex(long timeUs) { nextSubtitleEventIndex = subtitle == null ? -1 : subtitle.getNextEventTimeIndex(timeUs); } private long getNextEventTime() { return ((nextSubtitleEventIndex == -1) || (nextSubtitleEventIndex >= subtitle.getEventTimeCount())) ? Long.MAX_VALUE : (subtitle.getEventTime(nextSubtitleEventIndex)); } private void resetSampleData() { if (sampleHolder.data != null) { sampleHolder.data.position(0); } } private void updateTextRenderer(long timeUs) { String text = subtitle.getText(timeUs); log("updateTextRenderer; text=: " + text); if (textRendererHandler != null) { textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, text).sendToTarget(); } else { invokeTextRenderer(text); } } private void clearTextRenderer() { log("clearTextRenderer"); if (textRendererHandler != null) { textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, null).sendToTarget(); } else { invokeTextRenderer(null); } } @Override public boolean handleMessage(Message msg) { switch (msg.what) { case MSG_UPDATE_OVERLAY: invokeTextRenderer((String) msg.obj); return true; } return false; } private void invokeTextRenderer(String text) { textRenderer.onText(text); } private void log(String logMessage) { if (VerboseLogUtil.isTagEnabled(TAG)) { Log.v(TAG, "type=" + AdaptationSet.TYPE_TEXT + ", " + logMessage); } } }