diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DebugTrackRenderer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DebugTrackRenderer.java index 8093bad814..d848dd3908 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DebugTrackRenderer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DebugTrackRenderer.java @@ -68,10 +68,10 @@ import android.widget.TextView; } @Override - protected void doSomeWork(long timeUs) throws ExoPlaybackException { + protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { maybeFail(); - if (timeUs < currentPositionUs || timeUs > currentPositionUs + 1000000) { - currentPositionUs = timeUs; + if (positionUs < currentPositionUs || positionUs > currentPositionUs + 1000000) { + currentPositionUs = positionUs; textView.post(this); } } diff --git a/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java b/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java index 91bcac53ce..9131c4816c 100644 --- a/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java +++ b/library/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java @@ -166,9 +166,9 @@ public class DefaultLoadControl implements LoadControl { // Update the loader state. int loaderBufferState = getLoaderBufferState(playbackPositionUs, nextLoadPositionUs); LoaderState loaderState = loaderStates.get(loader); - boolean loaderStateChanged = loaderState.bufferState != loaderBufferState || - loaderState.nextLoadPositionUs != nextLoadPositionUs || loaderState.loading != loading || - loaderState.failed != failed; + boolean loaderStateChanged = loaderState.bufferState != loaderBufferState + || loaderState.nextLoadPositionUs != nextLoadPositionUs || loaderState.loading != loading + || loaderState.failed != failed; if (loaderStateChanged) { loaderState.bufferState = loaderBufferState; loaderState.nextLoadPositionUs = nextLoadPositionUs; @@ -214,17 +214,17 @@ public class DefaultLoadControl implements LoadControl { private void updateControlState() { boolean loading = false; boolean failed = false; - boolean finished = true; + boolean haveNextLoadPosition = false; int highestState = bufferPoolState; for (int i = 0; i < loaders.size(); i++) { LoaderState loaderState = loaderStates.get(loaders.get(i)); loading |= loaderState.loading; failed |= loaderState.failed; - finished &= loaderState.nextLoadPositionUs == -1; + haveNextLoadPosition |= loaderState.nextLoadPositionUs != -1; highestState = Math.max(highestState, loaderState.bufferState); } - fillingBuffers = !loaders.isEmpty() && !finished && !failed + fillingBuffers = !loaders.isEmpty() && !failed && (loading || haveNextLoadPosition) && (highestState == BELOW_LOW_WATERMARK || (highestState == BETWEEN_WATERMARKS && fillingBuffers)); if (fillingBuffers && !streamingPrioritySet) { diff --git a/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java index 4bafdd07b8..4dd5ef4a42 100644 --- a/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java @@ -40,12 +40,12 @@ public class DummyTrackRenderer extends TrackRenderer { } @Override - protected void seekTo(long timeUs) { + protected void seekTo(long positionUs) { throw new IllegalStateException(); } @Override - protected void doSomeWork(long timeUs) { + protected void doSomeWork(long positionUs, long elapsedRealtimeUs) { throw new IllegalStateException(); } diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java index 0184ea9956..2dfac29519 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -77,6 +77,7 @@ import java.util.List; private int state; private int customMessagesSent = 0; private int customMessagesProcessed = 0; + private long elapsedRealtimeUs; private volatile long durationUs; private volatile long positionUs; @@ -383,7 +384,8 @@ import java.util.List; positionUs = timeSourceTrackRenderer != null && enabledRenderers.contains(timeSourceTrackRenderer) ? timeSourceTrackRenderer.getCurrentPositionUs() : - mediaClock.getTimeUs(); + mediaClock.getPositionUs(); + elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; } private void doSomeWork() throws ExoPlaybackException { @@ -399,7 +401,7 @@ import java.util.List; // TODO: Each renderer should return the maximum delay before which it wishes to be // invoked again. The minimum of these values should then be used as the delay before the next // invocation of this method. - renderer.doSomeWork(positionUs); + renderer.doSomeWork(positionUs, elapsedRealtimeUs); isEnded = isEnded && renderer.isEnded(); allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer); @@ -462,7 +464,7 @@ import java.util.List; rebuffering = false; positionUs = positionMs * 1000L; mediaClock.stop(); - mediaClock.setTimeUs(positionUs); + mediaClock.setPositionUs(positionUs); if (state == ExoPlayer.STATE_IDLE || state == ExoPlayer.STATE_PREPARING) { return; } @@ -582,7 +584,7 @@ import java.util.List; if (renderer == timeSourceTrackRenderer) { // We've been using timeSourceTrackRenderer to advance the current position, but it's // being disabled. Sync mediaClock so that it can take over timing responsibilities. - mediaClock.setTimeUs(renderer.getCurrentPositionUs()); + mediaClock.setPositionUs(renderer.getCurrentPositionUs()); } ensureStopped(renderer); enabledRenderers.remove(renderer); diff --git a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java index 63afbdf0f5..0fc39b0e1a 100644 --- a/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/FrameworkSampleSource.java @@ -50,7 +50,7 @@ public final class FrameworkSampleSource implements SampleSource { private int[] trackStates; private boolean[] pendingDiscontinuities; - private long seekTimeUs; + private long seekPositionUs; public FrameworkSampleSource(Context context, Uri uri, Map headers, int downstreamRendererCount) { @@ -94,16 +94,16 @@ public final class FrameworkSampleSource implements SampleSource { } @Override - public void enable(int track, long timeUs) { + public void enable(int track, long positionUs) { Assertions.checkState(prepared); Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED); trackStates[track] = TRACK_STATE_ENABLED; extractor.selectTrack(track); - seekToUs(timeUs); + seekToUs(positionUs); } @Override - public boolean continueBuffering(long playbackPositionUs) { + public boolean continueBuffering(long positionUs) { // MediaExtractor takes care of buffering and blocks until it has samples, so we can always // return true here. Although note that the blocking behavior is itself as bug, as per the // TODO further up this file. This method will need to return something else as part of fixing @@ -112,7 +112,7 @@ public final class FrameworkSampleSource implements SampleSource { } @Override - public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, + public int readData(int track, long positionUs, MediaFormatHolder formatHolder, SampleHolder sampleHolder, boolean onlyReadDiscontinuity) { Assertions.checkState(prepared); Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED); @@ -144,7 +144,7 @@ public final class FrameworkSampleSource implements SampleSource { if ((sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0) { sampleHolder.cryptoInfo.setFromExtractorV16(extractor); } - seekTimeUs = -1; + seekPositionUs = -1; extractor.advance(); return SAMPLE_READ; } else { @@ -168,13 +168,13 @@ public final class FrameworkSampleSource implements SampleSource { } @Override - public void seekToUs(long timeUs) { + public void seekToUs(long positionUs) { Assertions.checkState(prepared); - if (seekTimeUs != timeUs) { + if (seekPositionUs != positionUs) { // Avoid duplicate calls to the underlying extractor's seek method in the case that there // have been no interleaving calls to advance. - seekTimeUs = timeUs; - extractor.seekTo(timeUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); + seekPositionUs = positionUs; + extractor.seekTo(positionUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); for (int i = 0; i < trackStates.length; ++i) { if (trackStates[i] != TRACK_STATE_DISABLED) { pendingDiscontinuities[i] = true; diff --git a/library/src/main/java/com/google/android/exoplayer/LoadControl.java b/library/src/main/java/com/google/android/exoplayer/LoadControl.java index edc6ff023f..df6130017f 100644 --- a/library/src/main/java/com/google/android/exoplayer/LoadControl.java +++ b/library/src/main/java/com/google/android/exoplayer/LoadControl.java @@ -65,9 +65,10 @@ public interface LoadControl { * * @param loader The loader invoking the update. * @param playbackPositionUs The loader's playback position. - * @param nextLoadPositionUs The loader's next load position, or -1 if finished. + * @param nextLoadPositionUs The loader's next load position. -1 if finished, failed, or if the + * next load position is not yet known. * @param loading Whether the loader is currently loading data. - * @param failed Whether the loader has failed, meaning it does not wish to load more data. + * @param failed Whether the loader has failed. * @return True if the loader is allowed to start its next load. False otherwise. */ boolean update(Object loader, long playbackPositionUs, long nextLoadPositionUs, diff --git a/library/src/main/java/com/google/android/exoplayer/MediaClock.java b/library/src/main/java/com/google/android/exoplayer/MediaClock.java index 9abd3c1f03..c2696e3b74 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaClock.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaClock.java @@ -29,10 +29,10 @@ import android.os.SystemClock; /** * The media time when the clock was last set or stopped. */ - private long timeUs; + private long positionUs; /** - * The difference between {@link SystemClock#elapsedRealtime()} and {@link #timeUs} + * The difference between {@link SystemClock#elapsedRealtime()} and {@link #positionUs} * when the clock was last set or started. */ private long deltaUs; @@ -43,7 +43,7 @@ import android.os.SystemClock; public void start() { if (!started) { started = true; - deltaUs = elapsedRealtimeMinus(timeUs); + deltaUs = elapsedRealtimeMinus(positionUs); } } @@ -52,28 +52,28 @@ import android.os.SystemClock; */ public void stop() { if (started) { - timeUs = elapsedRealtimeMinus(deltaUs); + positionUs = elapsedRealtimeMinus(deltaUs); started = false; } } /** - * @param timeUs The time to set in microseconds. + * @param timeUs The position to set in microseconds. */ - public void setTimeUs(long timeUs) { - this.timeUs = timeUs; + public void setPositionUs(long timeUs) { + this.positionUs = timeUs; deltaUs = elapsedRealtimeMinus(timeUs); } /** - * @return The current time in microseconds. + * @return The current position in microseconds. */ - public long getTimeUs() { - return started ? elapsedRealtimeMinus(deltaUs) : timeUs; + public long getPositionUs() { + return started ? elapsedRealtimeMinus(deltaUs) : positionUs; } - private long elapsedRealtimeMinus(long microSeconds) { - return SystemClock.elapsedRealtime() * 1000 - microSeconds; + private long elapsedRealtimeMinus(long toSubtractUs) { + return SystemClock.elapsedRealtime() * 1000 - toSubtractUs; } } diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java index 8a75331465..5027cb7830 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -94,10 +94,18 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { /** * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more - * than this amount. This is a fail safe that should not be required on correctly functioning - * devices. + * than this amount. + *

+ * This is a fail safe that should not be required on correctly functioning devices. */ - private static final long MAX_AUDIO_TIMSTAMP_OFFSET_US = 10 * MICROS_PER_SECOND; + private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 10 * MICROS_PER_SECOND; + + /** + * AudioTrack latencies are deemed impossibly large if they are greater than this amount. + *

+ * This is a fail safe that should not be required on correctly functioning devices. + */ + private static final long MAX_AUDIO_TRACK_LATENCY_US = 10 * MICROS_PER_SECOND; private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10; private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000; @@ -261,14 +269,14 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { } @Override - protected void onEnabled(long timeUs, boolean joining) { - super.onEnabled(timeUs, joining); + protected void onEnabled(long positionUs, boolean joining) { + super.onEnabled(positionUs, joining); lastReportedCurrentPositionUs = Long.MIN_VALUE; } @Override - protected void doSomeWork(long timeUs) throws ExoPlaybackException { - super.doSomeWork(timeUs); + protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + super.doSomeWork(positionUs, elapsedRealtimeUs); maybeSampleSyncParams(); } @@ -515,7 +523,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { if (audioTimestampUs < audioTrackResumeSystemTimeUs) { // The timestamp corresponds to a time before the track was most recently resumed. audioTimestampSet = false; - } else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMSTAMP_OFFSET_US) { + } else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) { // The timestamp time base is probably wrong. audioTimestampSet = false; Log.w(TAG, "Spurious audio timestamp: " + audioTimestampCompat.getFramePosition() + ", " @@ -531,6 +539,11 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { framesToDurationUs(bufferSize / frameSize); // Sanity check that the latency is non-negative. audioTrackLatencyUs = Math.max(audioTrackLatencyUs, 0); + // Sanity check that the latency isn't too large. + if (audioTrackLatencyUs > MAX_AUDIO_TRACK_LATENCY_US) { + Log.w(TAG, "Ignoring impossibly large audio latency: " + audioTrackLatencyUs); + audioTrackLatencyUs = 0; + } } catch (Exception e) { // The method existed, but doesn't work. Don't try again. audioTrackGetLatencyMethod = null; @@ -572,16 +585,16 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { } @Override - protected void seekTo(long timeUs) throws ExoPlaybackException { - super.seekTo(timeUs); + protected void seekTo(long positionUs) throws ExoPlaybackException { + super.seekTo(positionUs); // TODO: Try and re-use the same AudioTrack instance once [redacted] is fixed. releaseAudioTrack(); lastReportedCurrentPositionUs = Long.MIN_VALUE; } @Override - protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer, - MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) + protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, + ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) throws ExoPlaybackException { if (shouldSkip) { codec.releaseOutputBuffer(bufferIndex, false); @@ -616,6 +629,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { // time and the number of bytes submitted. Also reset lastReportedCurrentPositionUs to // allow time to jump backwards if it really wants to. audioTrackStartMediaTimeUs += (bufferStartTime - expectedBufferStartTime); + audioTrackStartMediaTimeState = START_IN_SYNC; lastReportedCurrentPositionUs = Long.MIN_VALUE; } } diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index 4124a22c27..5e28f36d1b 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -217,13 +217,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } @Override - protected void onEnabled(long timeUs, boolean joining) { - source.enable(trackIndex, timeUs); + protected void onEnabled(long positionUs, boolean joining) { + source.enable(trackIndex, positionUs); sourceState = SOURCE_STATE_NOT_READY; inputStreamEnded = false; outputStreamEnded = false; waitingForKeys = false; - currentPositionUs = timeUs; + currentPositionUs = positionUs; } /** @@ -367,9 +367,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } @Override - protected void seekTo(long timeUs) throws ExoPlaybackException { - currentPositionUs = timeUs; - source.seekToUs(timeUs); + protected void seekTo(long positionUs) throws ExoPlaybackException { + currentPositionUs = positionUs; + source.seekToUs(positionUs); sourceState = SOURCE_STATE_NOT_READY; inputStreamEnded = false; outputStreamEnded = false; @@ -387,22 +387,22 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } @Override - protected void doSomeWork(long timeUs) throws ExoPlaybackException { + protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { try { - sourceState = source.continueBuffering(timeUs) + sourceState = source.continueBuffering(positionUs) ? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState) : SOURCE_STATE_NOT_READY; checkForDiscontinuity(); if (format == null) { readFormat(); } else if (codec == null && !shouldInitCodec() && getState() == TrackRenderer.STATE_STARTED) { - discardSamples(timeUs); + discardSamples(positionUs); } else { if (codec == null && shouldInitCodec()) { maybeInitCodec(); } if (codec != null) { - while (drainOutputBuffer(timeUs)) {} + while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} if (feedInputBuffer(true)) { while (feedInputBuffer(false)) {} } @@ -421,10 +421,10 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { } } - private void discardSamples(long timeUs) throws IOException, ExoPlaybackException { + private void discardSamples(long positionUs) throws IOException, ExoPlaybackException { sampleHolder.data = null; int result = SampleSource.SAMPLE_READ; - while (result == SampleSource.SAMPLE_READ && currentPositionUs <= timeUs) { + while (result == SampleSource.SAMPLE_READ && currentPositionUs <= positionUs) { result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false); if (result == SampleSource.SAMPLE_READ) { if (!sampleHolder.decodeOnly) { @@ -469,7 +469,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { /** * @param firstFeed True if this is the first call to this method from the current invocation of - * {@link #doSomeWork(long)}. False otherwise. + * {@link #doSomeWork(long, long)}. False otherwise. * @return True if it may be possible to feed more input data. False otherwise. * @throws IOException If an error occurs reading data from the upstream source. * @throws ExoPlaybackException If an error occurs feeding the input buffer. @@ -694,7 +694,8 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { * @return True if it may be possible to drain more output data. False otherwise. * @throws ExoPlaybackException If an error occurs draining the output buffer. */ - private boolean drainOutputBuffer(long timeUs) throws ExoPlaybackException { + private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) + throws ExoPlaybackException { if (outputStreamEnded) { return false; } @@ -722,8 +723,8 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { boolean decodeOnly = decodeOnlyPresentationTimestamps.contains( outputBufferInfo.presentationTimeUs); - if (processOutputBuffer(timeUs, codec, outputBuffers[outputIndex], outputBufferInfo, - outputIndex, decodeOnly)) { + if (processOutputBuffer(positionUs, elapsedRealtimeUs, codec, outputBuffers[outputIndex], + outputBufferInfo, outputIndex, decodeOnly)) { if (decodeOnly) { decodeOnlyPresentationTimestamps.remove(outputBufferInfo.presentationTimeUs); } else { @@ -743,9 +744,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { * longer required. False otherwise. * @throws ExoPlaybackException If an error occurs processing the output buffer. */ - protected abstract boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer, - MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) - throws ExoPlaybackException; + protected abstract boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, + MediaCodec codec, ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, + boolean shouldSkip) throws ExoPlaybackException; /** * Returns the name of the secure variant of a given decoder. diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java index 565ab41723..a19be59df1 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -225,8 +225,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { } @Override - protected void onEnabled(long startTimeUs, boolean joining) { - super.onEnabled(startTimeUs, joining); + protected void onEnabled(long positionUs, boolean joining) { + super.onEnabled(positionUs, joining); renderedFirstFrame = false; if (joining && allowedJoiningTimeUs > 0) { joiningDeadlineUs = SystemClock.elapsedRealtime() * 1000L + allowedJoiningTimeUs; @@ -234,8 +234,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { } @Override - protected void seekTo(long timeUs) throws ExoPlaybackException { - super.seekTo(timeUs); + protected void seekTo(long positionUs) throws ExoPlaybackException { + super.seekTo(positionUs); renderedFirstFrame = false; joiningDeadlineUs = -1; } @@ -354,14 +354,15 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { } @Override - protected boolean processOutputBuffer(long timeUs, MediaCodec codec, ByteBuffer buffer, - MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) { + protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, + ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) { if (shouldSkip) { skipOutputBuffer(codec, bufferIndex); return true; } - long earlyUs = bufferInfo.presentationTimeUs - timeUs; + long elapsedSinceStartOfLoop = SystemClock.elapsedRealtime() * 1000 - elapsedRealtimeUs; + long earlyUs = bufferInfo.presentationTimeUs - positionUs - elapsedSinceStartOfLoop; if (earlyUs < -30000) { // We're more than 30ms late rendering the frame. dropOutputBuffer(codec, bufferIndex); diff --git a/library/src/main/java/com/google/android/exoplayer/ParserException.java b/library/src/main/java/com/google/android/exoplayer/ParserException.java index f3830bcba7..ce47f8aa16 100644 --- a/library/src/main/java/com/google/android/exoplayer/ParserException.java +++ b/library/src/main/java/com/google/android/exoplayer/ParserException.java @@ -26,8 +26,12 @@ public class ParserException extends IOException { super(message); } - public ParserException(Exception cause) { + public ParserException(Throwable cause) { super(cause); } + public ParserException(String message, Throwable cause) { + super(message, cause); + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/SampleSource.java b/library/src/main/java/com/google/android/exoplayer/SampleSource.java index 2f26d30e9a..9a3d40819b 100644 --- a/library/src/main/java/com/google/android/exoplayer/SampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SampleSource.java @@ -85,9 +85,9 @@ public interface SampleSource { * This method should not be called until after the source has been successfully prepared. * * @param track The track to enable. - * @param timeUs The player's current playback position. + * @param positionUs The player's current playback position. */ - public void enable(int track, long timeUs); + public void enable(int track, long positionUs); /** * Disable the specified track. @@ -101,12 +101,12 @@ public interface SampleSource { /** * Indicates to the source that it should still be buffering data. * - * @param playbackPositionUs The current playback position. + * @param positionUs The current playback position. * @return True if the source has available samples, or if the end of the stream has been reached. * False if more data needs to be buffered for samples to become available. * @throws IOException If an error occurred reading from the source. */ - public boolean continueBuffering(long playbackPositionUs) throws IOException; + public boolean continueBuffering(long positionUs) throws IOException; /** * Attempts to read either a sample, a new format or or a discontinuity from the source. @@ -118,7 +118,7 @@ public interface SampleSource { * than the one for which data was requested. * * @param track The track from which to read. - * @param playbackPositionUs The current playback position. + * @param positionUs The current playback position. * @param formatHolder A {@link MediaFormatHolder} object to populate in the case of a new format. * @param sampleHolder A {@link SampleHolder} object to populate in the case of a new sample. If * the caller requires the sample data then it must ensure that {@link SampleHolder#data} @@ -129,7 +129,7 @@ public interface SampleSource { * {@link #DISCONTINUITY_READ}, {@link #NOTHING_READ} or {@link #END_OF_STREAM}. * @throws IOException If an error occurred reading from the source. */ - public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, + public int readData(int track, long positionUs, MediaFormatHolder formatHolder, SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException; /** @@ -137,16 +137,16 @@ public interface SampleSource { *

* This method should not be called until after the source has been successfully prepared. * - * @param timeUs The seek position in microseconds. + * @param positionUs The seek position in microseconds. */ - public void seekToUs(long timeUs); + public void seekToUs(long positionUs); /** * Returns an estimate of the position up to which data is buffered. *

* This method should not be called until after the source has been successfully prepared. * - * @return An estimate of the absolute position in micro-seconds up to which data is buffered, + * @return An estimate of the absolute position in microseconds up to which data is buffered, * or {@link TrackRenderer#END_OF_TRACK_US} if data is buffered to the end of the stream, or * {@link TrackRenderer#UNKNOWN_TIME_US} if no estimate is available. */ diff --git a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java index f27433d06e..66e20291f7 100644 --- a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer; import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer.util.Assertions; +import android.os.SystemClock; + /** * Renders a single component of media. * @@ -59,7 +61,7 @@ public abstract class TrackRenderer implements ExoPlayerComponent { */ protected static final int STATE_ENABLED = 2; /** - * The renderer is started. Calls to {@link #doSomeWork(long)} should cause the media to be + * The renderer is started. Calls to {@link #doSomeWork(long, long)} should cause the media to be * rendered. */ protected static final int STATE_STARTED = 3; @@ -83,9 +85,9 @@ public abstract class TrackRenderer implements ExoPlayerComponent { /** * A time source renderer is a renderer that, when started, advances its own playback position. * This means that {@link #getCurrentPositionUs()} will return increasing positions independently - * to increasing values being passed to {@link #doSomeWork(long)}. A player may have at most one - * time source renderer. If provided, the player will use such a renderer as its source of time - * during playback. + * to increasing values being passed to {@link #doSomeWork(long, long)}. A player may have at most + * one time source renderer. If provided, the player will use such a renderer as its source of + * time during playback. *

* This method may be called when the renderer is in any state. * @@ -136,15 +138,15 @@ public abstract class TrackRenderer implements ExoPlayerComponent { /** * Enable the renderer. * - * @param timeUs The player's current position. + * @param positionUs The player's current position. * @param joining Whether this renderer is being enabled to join an ongoing playback. If true * then {@link #start} must be called immediately after this method returns (unless a * {@link ExoPlaybackException} is thrown). */ - /* package */ final void enable(long timeUs, boolean joining) throws ExoPlaybackException { + /* package */ final void enable(long positionUs, boolean joining) throws ExoPlaybackException { Assertions.checkState(state == TrackRenderer.STATE_PREPARED); state = TrackRenderer.STATE_ENABLED; - onEnabled(timeUs, joining); + onEnabled(positionUs, joining); } /** @@ -152,18 +154,18 @@ public abstract class TrackRenderer implements ExoPlayerComponent { *

* The default implementation is a no-op. * - * @param timeUs The player's current position. + * @param positionUs The player's current position. * @param joining Whether this renderer is being enabled to join an ongoing playback. If true * then {@link #onStarted} is guaranteed to be called immediately after this method returns * (unless a {@link ExoPlaybackException} is thrown). * @throws ExoPlaybackException If an error occurs. */ - protected void onEnabled(long timeUs, boolean joining) throws ExoPlaybackException { + protected void onEnabled(long positionUs, boolean joining) throws ExoPlaybackException { // Do nothing. } /** - * Starts the renderer, meaning that calls to {@link #doSomeWork(long)} will cause the + * Starts the renderer, meaning that calls to {@link #doSomeWork(long, long)} will cause the * track to be rendered. */ /* package */ final void start() throws ExoPlaybackException { @@ -289,10 +291,14 @@ public abstract class TrackRenderer implements ExoPlayerComponent { * This method may be called when the renderer is in the following states: * {@link #STATE_ENABLED}, {@link #STATE_STARTED} * - * @param timeUs The current playback time. + * @param positionUs The current media time in microseconds, measured at the start of the + * current iteration of the rendering loop. + * @param elapsedRealtimeUs {@link SystemClock#elapsedRealtime()} in microseconds, measured at + * the start of the current iteration of the rendering loop. * @throws ExoPlaybackException If an error occurs. */ - protected abstract void doSomeWork(long timeUs) throws ExoPlaybackException; + protected abstract void doSomeWork(long positionUs, long elapsedRealtimeUs) + throws ExoPlaybackException; /** * Returns the duration of the media being rendered. @@ -300,7 +306,7 @@ public abstract class TrackRenderer implements ExoPlayerComponent { * This method may be called when the renderer is in the following states: * {@link #STATE_PREPARED}, {@link #STATE_ENABLED}, {@link #STATE_STARTED} * - * @return The duration of the track in micro-seconds, or {@link #MATCH_LONGEST_US} if + * @return The duration of the track in microseconds, or {@link #MATCH_LONGEST_US} if * the track's duration should match that of the longest track whose duration is known, or * or {@link #UNKNOWN_TIME_US} if the duration is not known. */ @@ -312,17 +318,17 @@ public abstract class TrackRenderer implements ExoPlayerComponent { * This method may be called when the renderer is in the following states: * {@link #STATE_ENABLED}, {@link #STATE_STARTED} * - * @return The current playback position in micro-seconds. + * @return The current playback position in microseconds. */ protected abstract long getCurrentPositionUs(); /** - * Returns an estimate of the absolute position in micro-seconds up to which data is buffered. + * Returns an estimate of the absolute position in microseconds up to which data is buffered. *

* This method may be called when the renderer is in the following states: * {@link #STATE_ENABLED}, {@link #STATE_STARTED} * - * @return An estimate of the absolute position in micro-seconds up to which data is buffered, + * @return An estimate of the absolute position in microseconds up to which data is buffered, * or {@link #END_OF_TRACK_US} if the track is fully buffered, or {@link #UNKNOWN_TIME_US} if * no estimate is available. */ @@ -334,10 +340,10 @@ public abstract class TrackRenderer implements ExoPlayerComponent { * This method may be called when the renderer is in the following states: * {@link #STATE_ENABLED} * - * @param timeUs The desired time in micro-seconds. + * @param positionUs The desired playback position in microseconds. * @throws ExoPlaybackException If an error occurs. */ - protected abstract void seekTo(long timeUs) throws ExoPlaybackException; + protected abstract void seekTo(long positionUs) throws ExoPlaybackException; @Override public void handleMessage(int what, Object object) throws ExoPlaybackException { diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index f2f0ce031a..f7d556f3c8 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java @@ -154,7 +154,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { private int state; private long downstreamPositionUs; private long lastSeekPositionUs; - private long pendingResetTime; + private long pendingResetPositionUs; private long lastPerformedBufferOperation; private boolean pendingDiscontinuity; @@ -219,7 +219,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } @Override - public void enable(int track, long timeUs) { + public void enable(int track, long positionUs) { Assertions.checkState(state == STATE_PREPARED); Assertions.checkState(track == 0); state = STATE_ENABLED; @@ -227,9 +227,9 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { loadControl.register(this, bufferSizeContribution); downstreamFormat = null; downstreamMediaFormat = null; - downstreamPositionUs = timeUs; - lastSeekPositionUs = timeUs; - restartFrom(timeUs); + downstreamPositionUs = positionUs; + lastSeekPositionUs = positionUs; + restartFrom(positionUs); } @Override @@ -253,10 +253,10 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } @Override - public boolean continueBuffering(long playbackPositionUs) throws IOException { + public boolean continueBuffering(long positionUs) throws IOException { Assertions.checkState(state == STATE_ENABLED); - downstreamPositionUs = playbackPositionUs; - chunkSource.continueBuffering(playbackPositionUs); + downstreamPositionUs = positionUs; + chunkSource.continueBuffering(positionUs); updateLoadControl(); if (isPendingReset() || mediaChunks.isEmpty()) { return false; @@ -271,7 +271,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } @Override - public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, + public int readData(int track, long positionUs, MediaFormatHolder formatHolder, SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException { Assertions.checkState(state == STATE_ENABLED); Assertions.checkState(track == 0); @@ -285,7 +285,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { return NOTHING_READ; } - downstreamPositionUs = playbackPositionUs; + downstreamPositionUs = positionUs; if (isPendingReset()) { if (currentLoadableException != null) { throw currentLoadableException; @@ -304,7 +304,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { discardDownstreamMediaChunk(); mediaChunk = mediaChunks.getFirst(); mediaChunk.seekToStart(); - return readData(track, playbackPositionUs, formatHolder, sampleHolder, false); + return readData(track, positionUs, formatHolder, sampleHolder, false); } else if (mediaChunk.isLastChunk()) { return END_OF_STREAM; } @@ -350,32 +350,32 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } @Override - public void seekToUs(long timeUs) { + public void seekToUs(long positionUs) { Assertions.checkState(state == STATE_ENABLED); - downstreamPositionUs = timeUs; - lastSeekPositionUs = timeUs; - if (pendingResetTime == timeUs) { + downstreamPositionUs = positionUs; + lastSeekPositionUs = positionUs; + if (pendingResetPositionUs == positionUs) { return; } - MediaChunk mediaChunk = getMediaChunk(timeUs); + MediaChunk mediaChunk = getMediaChunk(positionUs); if (mediaChunk == null) { - restartFrom(timeUs); + restartFrom(positionUs); pendingDiscontinuity = true; } else { - pendingDiscontinuity |= mediaChunk.seekTo(timeUs, mediaChunk == mediaChunks.getFirst()); + pendingDiscontinuity |= mediaChunk.seekTo(positionUs, mediaChunk == mediaChunks.getFirst()); discardDownstreamMediaChunks(mediaChunk); updateLoadControl(); } } - private MediaChunk getMediaChunk(long timeUs) { + private MediaChunk getMediaChunk(long positionUs) { Iterator mediaChunkIterator = mediaChunks.iterator(); while (mediaChunkIterator.hasNext()) { MediaChunk mediaChunk = mediaChunkIterator.next(); - if (timeUs < mediaChunk.startTimeUs) { + if (positionUs < mediaChunk.startTimeUs) { return null; - } else if (mediaChunk.isLastChunk() || timeUs < mediaChunk.endTimeUs) { + } else if (mediaChunk.isLastChunk() || positionUs < mediaChunk.endTimeUs) { return mediaChunk; } } @@ -386,7 +386,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { public long getBufferedPositionUs() { Assertions.checkState(state == STATE_ENABLED); if (isPendingReset()) { - return pendingResetTime; + return pendingResetPositionUs; } MediaChunk mediaChunk = mediaChunks.getLast(); Chunk currentLoadable = currentLoadableHolder.chunk; @@ -448,7 +448,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } clearCurrentLoadable(); if (state == STATE_ENABLED) { - restartFrom(pendingResetTime); + restartFrom(pendingResetPositionUs); } else { clearMediaChunks(); loadControl.trimAllocator(); @@ -476,8 +476,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { // no-op } - private void restartFrom(long timeUs) { - pendingResetTime = timeUs; + private void restartFrom(long positionUs) { + pendingResetPositionUs = positionUs; if (loader.isLoading()) { loader.cancelLoading(); } else { @@ -499,23 +499,40 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } private void updateLoadControl() { - long loadPositionUs; - if (isPendingReset()) { - loadPositionUs = pendingResetTime; - } else { - MediaChunk lastMediaChunk = mediaChunks.getLast(); - loadPositionUs = lastMediaChunk.nextChunkIndex == -1 ? -1 : lastMediaChunk.endTimeUs; - } - - boolean isBackedOff = currentLoadableException != null && !currentLoadableExceptionFatal; - boolean nextLoader = loadControl.update(this, downstreamPositionUs, loadPositionUs, - isBackedOff || loader.isLoading(), currentLoadableExceptionFatal); - if (currentLoadableExceptionFatal) { + // We've failed, but we still need to update the control with our current state. + loadControl.update(this, downstreamPositionUs, -1, false, true); return; } long now = SystemClock.elapsedRealtime(); + long nextLoadPositionUs = getNextLoadPositionUs(); + boolean isBackedOff = currentLoadableException != null; + boolean loadingOrBackedOff = loader.isLoading() || isBackedOff; + + // If we're not loading or backed off, evaluate the operation if (a) we don't have the next + // chunk yet and we're not finished, or (b) if the last evaluation was over 2000ms ago. + if (!loadingOrBackedOff && ((currentLoadableHolder.chunk == null && nextLoadPositionUs != -1) + || (now - lastPerformedBufferOperation > 2000))) { + // Perform the evaluation. + lastPerformedBufferOperation = now; + currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); + chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, + downstreamPositionUs, currentLoadableHolder); + boolean chunksDiscarded = discardUpstreamMediaChunks(currentLoadableHolder.queueSize); + // Update the next load position as appropriate. + if (currentLoadableHolder.chunk == null) { + // Set loadPosition to -1 to indicate that we don't have anything to load. + nextLoadPositionUs = -1; + } else if (chunksDiscarded) { + // Chunks were discarded, so we need to re-evaluate the load position. + nextLoadPositionUs = getNextLoadPositionUs(); + } + } + + // Update the control with our current state, and determine whether we're the next loader. + boolean nextLoader = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs, + loadingOrBackedOff, false); if (isBackedOff) { long elapsedMillis = now - currentLoadableExceptionTimestamp; @@ -525,17 +542,21 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { return; } - if (!loader.isLoading()) { - if (currentLoadableHolder.chunk == null || now - lastPerformedBufferOperation > 1000) { - lastPerformedBufferOperation = now; - currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); - chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs, - currentLoadableHolder); - discardUpstreamMediaChunks(currentLoadableHolder.queueSize); - } - if (nextLoader) { - maybeStartLoading(); - } + if (!loader.isLoading() && nextLoader) { + maybeStartLoading(); + } + } + + /** + * Gets the next load time, assuming that the next load starts where the previous chunk ended (or + * from the pending reset time, if there is one). + */ + private long getNextLoadPositionUs() { + if (isPendingReset()) { + return pendingResetPositionUs; + } else { + MediaChunk lastMediaChunk = mediaChunks.getLast(); + return lastMediaChunk.nextChunkIndex == -1 ? -1 : lastMediaChunk.endTimeUs; } } @@ -552,8 +573,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { Chunk backedOffChunk = currentLoadableHolder.chunk; if (!isMediaChunk(backedOffChunk)) { currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); - chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs, - currentLoadableHolder); + chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, + downstreamPositionUs, currentLoadableHolder); discardUpstreamMediaChunks(currentLoadableHolder.queueSize); if (currentLoadableHolder.chunk == backedOffChunk) { // Chunk was unchanged. Resume loading. @@ -577,7 +598,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { MediaChunk removedChunk = mediaChunks.removeLast(); Assertions.checkState(backedOffChunk == removedChunk); currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); - chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs, + chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, downstreamPositionUs, currentLoadableHolder); mediaChunks.add(removedChunk); @@ -603,8 +624,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { if (isMediaChunk(currentLoadable)) { MediaChunk mediaChunk = (MediaChunk) currentLoadable; if (isPendingReset()) { - mediaChunk.seekTo(pendingResetTime, false); - pendingResetTime = NO_RESET_PENDING; + mediaChunk.seekTo(pendingResetPositionUs, false); + pendingResetPositionUs = NO_RESET_PENDING; } mediaChunks.add(mediaChunk); notifyLoadStarted(mediaChunk.format.id, mediaChunk.trigger, false, @@ -652,10 +673,11 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { * Discard upstream media chunks until the queue length is equal to the length specified. * * @param queueLength The desired length of the queue. + * @return True if chunks were discarded. False otherwise. */ - private void discardUpstreamMediaChunks(int queueLength) { + private boolean discardUpstreamMediaChunks(int queueLength) { if (mediaChunks.size() <= queueLength) { - return; + return false; } long totalBytes = 0; long startTimeUs = 0; @@ -667,6 +689,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { removed.release(); } notifyUpstreamDiscarded(startTimeUs, endTimeUs, totalBytes); + return true; } private boolean isMediaChunk(Chunk chunk) { @@ -674,7 +697,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } private boolean isPendingReset() { - return pendingResetTime != NO_RESET_PENDING; + return pendingResetPositionUs != NO_RESET_PENDING; } private long getRetryDelayMillis(long errorCount) { @@ -757,13 +780,13 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } private void notifyDownstreamFormatChanged(final String formatId, final int trigger, - final long mediaTimeUs) { + final long positionUs) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { eventListener.onDownstreamFormatChanged(eventSourceId, formatId, trigger, - usToMs(mediaTimeUs)); + usToMs(positionUs)); } }); } 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 405b778209..f7f38d986a 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 @@ -115,43 +115,43 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { } @Override - protected void onEnabled(long timeUs, boolean joining) { - source.enable(trackIndex, timeUs); + protected void onEnabled(long positionUs, boolean joining) { + source.enable(trackIndex, positionUs); parserThread = new HandlerThread("textParser"); parserThread.start(); parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParser); - seekToInternal(timeUs); + seekToInternal(positionUs); } @Override - protected void seekTo(long timeUs) { - source.seekToUs(timeUs); - seekToInternal(timeUs); + protected void seekTo(long positionUs) { + source.seekToUs(positionUs); + seekToInternal(positionUs); } - private void seekToInternal(long timeUs) { + private void seekToInternal(long positionUs) { inputStreamEnded = false; - currentPositionUs = timeUs; - source.seekToUs(timeUs); - if (subtitle != null && (timeUs < subtitle.getStartTime() - || subtitle.getLastEventTime() <= timeUs)) { + currentPositionUs = positionUs; + source.seekToUs(positionUs); + if (subtitle != null && (positionUs < subtitle.getStartTime() + || subtitle.getLastEventTime() <= positionUs)) { subtitle = null; } parserHelper.flush(); clearTextRenderer(); - syncNextEventIndex(timeUs); + syncNextEventIndex(positionUs); textRendererNeedsUpdate = subtitle != null; } @Override - protected void doSomeWork(long timeUs) throws ExoPlaybackException { + protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { try { - source.continueBuffering(timeUs); + source.continueBuffering(positionUs); } catch (IOException e) { throw new ExoPlaybackException(e); } - currentPositionUs = timeUs; + currentPositionUs = positionUs; if (parserHelper.isParsing()) { return; @@ -169,13 +169,13 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { if (subtitle == null && dequeuedSubtitle != null) { // We've dequeued a new subtitle. Sync the event index and update the subtitle. subtitle = dequeuedSubtitle; - syncNextEventIndex(timeUs); + syncNextEventIndex(positionUs); 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) { + while (nextEventTimeUs <= positionUs) { nextSubtitleEventIndex++; nextEventTimeUs = getNextEventTime(); textRendererNeedsUpdate = true; @@ -191,7 +191,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { if (subtitle == null) { try { SampleHolder sampleHolder = parserHelper.getSampleHolder(); - int result = source.readData(trackIndex, timeUs, formatHolder, sampleHolder, false); + int result = source.readData(trackIndex, positionUs, formatHolder, sampleHolder, false); if (result == SampleSource.SAMPLE_READ) { parserHelper.startParseOperation(); } else if (result == SampleSource.END_OF_STREAM) { @@ -208,7 +208,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { if (subtitle == null) { clearTextRenderer(); } else { - updateTextRenderer(timeUs); + updateTextRenderer(positionUs); } } } @@ -256,8 +256,8 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { return true; } - private void syncNextEventIndex(long timeUs) { - nextSubtitleEventIndex = subtitle == null ? -1 : subtitle.getNextEventTimeIndex(timeUs); + private void syncNextEventIndex(long positionUs) { + nextSubtitleEventIndex = subtitle == null ? -1 : subtitle.getNextEventTimeIndex(positionUs); } private long getNextEventTime() { @@ -266,8 +266,8 @@ public class TextTrackRenderer extends TrackRenderer implements Callback { : (subtitle.getEventTime(nextSubtitleEventIndex)); } - private void updateTextRenderer(long timeUs) { - String text = subtitle.getText(timeUs); + private void updateTextRenderer(long positionUs) { + String text = subtitle.getText(positionUs); log("updateTextRenderer; text=: " + text); if (textRendererHandler != null) { textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, text).sendToTarget(); diff --git a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java index 2fd1850f53..82b41d3d8b 100644 --- a/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java +++ b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.text.ttml; +import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.text.Subtitle; import com.google.android.exoplayer.text.SubtitleParser; import com.google.android.exoplayer.util.MimeTypes; @@ -72,8 +73,23 @@ public class TtmlParser implements SubtitleParser { private static final int DEFAULT_TICKRATE = 1; private final XmlPullParserFactory xmlParserFactory; + private final boolean strictParsing; + /** + * Equivalent to {@code TtmlParser(true)}. + */ public TtmlParser() { + this(true); + } + + /** + * @param strictParsing If true, {@link #parse(InputStream, String, long)} will throw a + * {@link ParserException} if the stream contains invalid ttml. If false, the parser will + * make a best effort to ignore minor errors in the stream. Note however that a + * {@link ParserException} will still be thrown when this is not possible. + */ + public TtmlParser(boolean strictParsing) { + this.strictParsing = strictParsing; try { xmlParserFactory = XmlPullParserFactory.newInstance(); } catch (XmlPullParserException e) { @@ -89,21 +105,31 @@ public class TtmlParser implements SubtitleParser { xmlParser.setInput(inputStream, inputEncoding); TtmlSubtitle ttmlSubtitle = null; LinkedList nodeStack = new LinkedList(); - int unsupportedTagDepth = 0; + int unsupportedNodeDepth = 0; int eventType = xmlParser.getEventType(); while (eventType != XmlPullParser.END_DOCUMENT) { TtmlNode parent = nodeStack.peekLast(); - if (unsupportedTagDepth == 0) { + if (unsupportedNodeDepth == 0) { String name = xmlParser.getName(); if (eventType == XmlPullParser.START_TAG) { if (!isSupportedTag(name)) { - Log.w(TAG, "Ignoring unsupported tag: " + xmlParser.getName()); - unsupportedTagDepth++; + Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName()); + unsupportedNodeDepth++; } else { - TtmlNode node = parseNode(xmlParser, parent); - nodeStack.addLast(node); - if (parent != null) { - parent.addChild(node); + try { + TtmlNode node = parseNode(xmlParser, parent); + nodeStack.addLast(node); + if (parent != null) { + parent.addChild(node); + } + } catch (ParserException e) { + if (strictParsing) { + throw e; + } else { + Log.e(TAG, "Suppressing parser error", e); + // Treat the node (and by extension, all of its children) as unsupported. + unsupportedNodeDepth++; + } } } } else if (eventType == XmlPullParser.TEXT) { @@ -116,9 +142,9 @@ public class TtmlParser implements SubtitleParser { } } else { if (eventType == XmlPullParser.START_TAG) { - unsupportedTagDepth++; + unsupportedNodeDepth++; } else if (eventType == XmlPullParser.END_TAG) { - unsupportedTagDepth--; + unsupportedNodeDepth--; } } xmlParser.next(); @@ -126,7 +152,7 @@ public class TtmlParser implements SubtitleParser { } return ttmlSubtitle; } catch (XmlPullParserException xppe) { - throw new IOException("Unable to parse source", xppe); + throw new ParserException("Unable to parse source", xppe); } } @@ -135,7 +161,7 @@ public class TtmlParser implements SubtitleParser { return MimeTypes.APPLICATION_TTML.equals(mimeType); } - private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent) { + private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent) throws ParserException { long duration = 0; long startTime = TtmlNode.UNDEFINED_TIME; long endTime = TtmlNode.UNDEFINED_TIME; @@ -209,10 +235,10 @@ public class TtmlParser implements SubtitleParser { * @param subframeRate The sub-framerate of the stream * @param tickRate The tick rate of the stream. * @return The parsed timestamp in microseconds. - * @throws NumberFormatException If the given string does not contain a valid time expression. + * @throws ParserException If the given string does not contain a valid time expression. */ private static long parseTimeExpression(String time, int frameRate, int subframeRate, - int tickRate) { + int tickRate) throws ParserException { Matcher matcher = CLOCK_TIME.matcher(time); if (matcher.matches()) { String hours = matcher.group(1); @@ -250,7 +276,7 @@ public class TtmlParser implements SubtitleParser { } return (long) (offsetSeconds * 1000000); } - throw new NumberFormatException("Malformed time expression: " + time); + throw new ParserException("Malformed time expression: " + time); } }