diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java index 67330957ba4..cc3a1f9d6c8 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java @@ -45,6 +45,8 @@ public interface ChunkSource { * the supplied {@link MediaFormat}. Other implementations do nothing. *

* Only called when the source is enabled. + * + * @param out The {@link MediaFormat} on which the maximum video dimensions should be set. */ void getMaxVideoDimensions(MediaFormat out); diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java new file mode 100644 index 00000000000..ffb90eaefdf --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java @@ -0,0 +1,108 @@ +/* + * 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.chunk; + +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.TrackInfo; +import com.google.android.exoplayer.upstream.DataSource; +import com.google.android.exoplayer.upstream.DataSpec; + +import java.io.IOException; +import java.util.List; + +/** + * A chunk source that provides a single chunk containing a single sample. + *

+ * An example use case for this implementation is to act as the source for loading out-of-band + * subtitles, where subtitles for the entire video are delivered as a single file. + */ +public class SingleSampleChunkSource implements ChunkSource { + + private final DataSource dataSource; + private final DataSpec dataSpec; + private final Format format; + private final long durationUs; + private final MediaFormat mediaFormat; + private final TrackInfo trackInfo; + + /** + * @param dataSource A {@link DataSource} suitable for loading the sample data. + * @param dataSpec Defines the location of the sample. + * @param format The format of the sample. + * @param durationUs The duration of the sample in microseconds. + * @param mediaFormat The sample media format. May be null. + */ + public SingleSampleChunkSource(DataSource dataSource, DataSpec dataSpec, Format format, + long durationUs, MediaFormat mediaFormat) { + this.dataSource = dataSource; + this.dataSpec = dataSpec; + this.format = format; + this.durationUs = durationUs; + this.mediaFormat = mediaFormat; + trackInfo = new TrackInfo(format.mimeType, durationUs); + } + + @Override + public TrackInfo getTrackInfo() { + return trackInfo; + } + + @Override + public void getMaxVideoDimensions(MediaFormat out) { + // Do nothing. + } + + @Override + public void enable() { + // Do nothing. + } + + @Override + public void continueBuffering(long playbackPositionUs) { + // Do nothing. + } + + @Override + public void getChunkOperation(List queue, long seekPositionUs, + long playbackPositionUs, ChunkOperationHolder out) { + if (!queue.isEmpty()) { + // We've already provided the single sample. + return; + } + out.chunk = initChunk(); + } + + @Override + public void disable(List queue) { + // Do nothing. + } + + @Override + public IOException getError() { + return null; + } + + @Override + public void onChunkLoadError(Chunk chunk, Exception e) { + // Do nothing. + } + + private SingleSampleMediaChunk initChunk() { + return new SingleSampleMediaChunk(dataSource, dataSpec, format, 0, 0, durationUs, -1, + mediaFormat); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java index 4765a8e8b43..405b7782092 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/text/TextTrackRenderer.java @@ -20,20 +20,18 @@ 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.HandlerThread; 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 @@ -63,7 +61,6 @@ public interface TextRenderer { private final Handler textRendererHandler; private final TextRenderer textRenderer; private final SampleSource source; - private final SampleHolder sampleHolder; private final MediaFormatHolder formatHolder; private final SubtitleParser subtitleParser; @@ -73,6 +70,8 @@ public interface TextRenderer { private boolean inputStreamEnded; private Subtitle subtitle; + private SubtitleParserHelper parserHelper; + private HandlerThread parserThread; private int nextSubtitleEventIndex; private boolean textRendererNeedsUpdate; @@ -94,7 +93,6 @@ public TextTrackRenderer(SampleSource source, SubtitleParser subtitleParser, this.textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper, this); formatHolder = new MediaFormatHolder(); - sampleHolder = new SampleHolder(true); } @Override @@ -119,6 +117,9 @@ protected int doPrepare() throws ExoPlaybackException { @Override protected void onEnabled(long timeUs, boolean joining) { source.enable(trackIndex, timeUs); + parserThread = new HandlerThread("textParser"); + parserThread.start(); + parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParser); seekToInternal(timeUs); } @@ -136,7 +137,7 @@ private void seekToInternal(long timeUs) { || subtitle.getLastEventTime() <= timeUs)) { subtitle = null; } - resetSampleData(); + parserHelper.flush(); clearTextRenderer(); syncNextEventIndex(timeUs); textRendererNeedsUpdate = subtitle != null; @@ -152,9 +153,27 @@ protected void doSomeWork(long timeUs) throws ExoPlaybackException { currentPositionUs = timeUs; - // We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we advance - // to the next event. - if (subtitle != null) { + if (parserHelper.isParsing()) { + return; + } + + Subtitle dequeuedSubtitle = null; + if (subtitle == null) { + try { + dequeuedSubtitle = parserHelper.getAndClearResult(); + } catch (IOException e) { + throw new ExoPlaybackException(e); + } + } + + if (subtitle == null && dequeuedSubtitle != null) { + // We've dequeued a new subtitle. Sync the event index and update the subtitle. + subtitle = dequeuedSubtitle; + syncNextEventIndex(timeUs); + textRendererNeedsUpdate = true; + } else if (subtitle != null) { + // We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we + // advance to the next event. long nextEventTimeUs = getNextEventTime(); while (nextEventTimeUs <= timeUs) { nextSubtitleEventIndex++; @@ -170,26 +189,16 @@ protected void doSomeWork(long timeUs) throws ExoPlaybackException { // 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 { + SampleHolder sampleHolder = parserHelper.getSampleHolder(); 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, null, sampleHolder.timeUs); - syncNextEventIndex(timeUs); - textRendererNeedsUpdate = true; + parserHelper.startParseOperation(); } else if (result == SampleSource.END_OF_STREAM) { inputStreamEnded = true; } } catch (IOException e) { - resetSampleHolder = true; throw new ExoPlaybackException(e); - } finally { - if (resetSampleHolder) { - resetSampleData(); - } } } @@ -208,7 +217,9 @@ protected void doSomeWork(long timeUs) throws ExoPlaybackException { protected void onDisabled() { source.disable(trackIndex); subtitle = null; - resetSampleData(); + parserThread.quit(); + parserThread = null; + parserHelper = null; clearTextRenderer(); } @@ -255,12 +266,6 @@ private long getNextEventTime() { : (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); @@ -296,7 +301,7 @@ private void invokeTextRenderer(String text) { private void log(String logMessage) { if (VerboseLogUtil.isTagEnabled(TAG)) { - Log.v(TAG, "type=" + AdaptationSet.TYPE_TEXT + ", " + logMessage); + Log.v(TAG, logMessage); } } diff --git a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java index 01556360337..cc6bdc4ef4a 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java +++ b/library/src/main/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitle.java @@ -79,15 +79,21 @@ public long getLastEventTime() { @Override public String getText(long timeUs) { - StringBuilder subtitleStringBuilder = new StringBuilder(); + StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < cueTimesUs.length; i += 2) { if ((cueTimesUs[i] <= timeUs) && (timeUs < cueTimesUs[i + 1])) { - subtitleStringBuilder.append(cueText[i / 2]); + stringBuilder.append(cueText[i / 2]); } } - return (subtitleStringBuilder.length() > 0) ? subtitleStringBuilder.toString() : null; + int stringLength = stringBuilder.length(); + if (stringLength > 0 && stringBuilder.charAt(stringLength - 1) == '\n') { + // Adjust the length to remove the trailing newline character. + stringLength -= 1; + } + + return stringLength == 0 ? null : stringBuilder.substring(0, stringLength); } } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java index 41f758be8a1..ff3b7dda0d9 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java @@ -53,6 +53,15 @@ public final class DataSpec { */ public final String key; + /** + * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null. + * + * @param uri {@link #uri}. + */ + public DataSpec(Uri uri) { + this(uri, 0, C.LENGTH_UNBOUNDED, null); + } + /** * Construct a {@link DataSpec} for which {@link #uriIsFullStream} is true. *