From ac18ac087bf9d178580112edf386fb56016da786 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 9 Oct 2014 17:13:14 +0100 Subject: [PATCH 1/6] Fix missing ->IN_SYNC transition. --- .../google/android/exoplayer/MediaCodecAudioTrackRenderer.java | 1 + 1 file changed, 1 insertion(+) 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..f0613bc82c 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -616,6 +616,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; } } From 43712ce41c659181bf8f7f8b3d133732a5ace9be Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 9 Oct 2014 17:16:13 +0100 Subject: [PATCH 2/6] Cap AudioTrack latencies at 10 seconds and log a warning if too large. --- .../MediaCodecAudioTrackRenderer.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) 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 f0613bc82c..b59744d893 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; @@ -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; From d4e824634c85f4f2b6b0bfac200a7bbfeddd3aba Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 9 Oct 2014 17:17:43 +0100 Subject: [PATCH 3/6] Throw a checked exception rather than unchecked one. So that we actually catch it, rather than having the process crash! --- .../google/android/exoplayer/text/ttml/TtmlParser.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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..9c60db6b54 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; @@ -135,7 +136,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 +210,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 +251,7 @@ public class TtmlParser implements SubtitleParser { } return (long) (offsetSeconds * 1000000); } - throw new NumberFormatException("Malformed time expression: " + time); + throw new ParserException("Malformed time expression: " + time); } } From 3b4409ae0b1627d2dd238d15e2723c029b1ee181 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 9 Oct 2014 17:19:14 +0100 Subject: [PATCH 4/6] Allow relaxation of TTML validity requirement when parsing subtitles. --- .../android/exoplayer/ParserException.java | 6 ++- .../exoplayer/text/ttml/TtmlParser.java | 47 ++++++++++++++----- 2 files changed, 41 insertions(+), 12 deletions(-) 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/text/ttml/TtmlParser.java b/library/src/main/java/com/google/android/exoplayer/text/ttml/TtmlParser.java index 9c60db6b54..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 @@ -73,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) { @@ -90,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) { @@ -117,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(); @@ -127,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); } } From 027d9eefbdfdfbc74208376cc1834e54a8cd1d93 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 9 Oct 2014 17:26:01 +0100 Subject: [PATCH 5/6] Smoother playback #1. Propagate elapsedRealtimeUs to the video renderer. This allows the renderer to calculate and adjust for the elapsed time since the start of the current rendering loop. Typically this is <2ms, but there situations where it can go higher (normally when the video renderer ends up processing more than 1 output buffer in a single loop). Also made variable naming more consistent throughout the package. --- .../demo/full/player/DebugTrackRenderer.java | 6 +- .../android/exoplayer/DummyTrackRenderer.java | 4 +- .../exoplayer/ExoPlayerImplInternal.java | 10 +-- .../exoplayer/FrameworkSampleSource.java | 20 +++--- .../google/android/exoplayer/MediaClock.java | 24 +++---- .../MediaCodecAudioTrackRenderer.java | 16 ++--- .../exoplayer/MediaCodecTrackRenderer.java | 39 +++++----- .../MediaCodecVideoTrackRenderer.java | 15 ++-- .../android/exoplayer/SampleSource.java | 18 ++--- .../android/exoplayer/TrackRenderer.java | 42 ++++++----- .../exoplayer/chunk/ChunkSampleSource.java | 72 +++++++++---------- .../exoplayer/text/TextTrackRenderer.java | 46 ++++++------ 12 files changed, 161 insertions(+), 151 deletions(-) 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/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/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 b59744d893..5027cb7830 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -269,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(); } @@ -585,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); 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/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..a436077d5f 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 { @@ -501,7 +501,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { private void updateLoadControl() { long loadPositionUs; if (isPendingReset()) { - loadPositionUs = pendingResetTime; + loadPositionUs = pendingResetPositionUs; } else { MediaChunk lastMediaChunk = mediaChunks.getLast(); loadPositionUs = lastMediaChunk.nextChunkIndex == -1 ? -1 : lastMediaChunk.endTimeUs; @@ -529,8 +529,8 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { if (currentLoadableHolder.chunk == null || now - lastPerformedBufferOperation > 1000) { lastPerformedBufferOperation = now; currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); - chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs, - currentLoadableHolder); + chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, + downstreamPositionUs, currentLoadableHolder); discardUpstreamMediaChunks(currentLoadableHolder.queueSize); } if (nextLoader) { @@ -552,8 +552,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 +577,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 +603,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, @@ -674,7 +674,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 +757,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(); From 759431048d7c990c2c3ef8a28e73c8e3e843b680 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 9 Oct 2014 17:27:20 +0100 Subject: [PATCH 6/6] Treat "no chunk to load yet" in the same way as finished. The key change here is that nextLoadPositionUs is set to -1 if we're not loading but don't have a next chunk ready to load. This ensures that "missing chunks" in one stream don't prevent chunks in another stream from loading. This occurs in SmoothStreaming with TTML subtitles, where the chunks are sparse. --- .../android/exoplayer/DefaultLoadControl.java | 12 +-- .../google/android/exoplayer/LoadControl.java | 5 +- .../exoplayer/chunk/ChunkSampleSource.java | 73 ++++++++++++------- 3 files changed, 57 insertions(+), 33 deletions(-) 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/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/chunk/ChunkSampleSource.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java index a436077d5f..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 @@ -499,23 +499,40 @@ public class ChunkSampleSource implements SampleSource, Loader.Callback { } private void updateLoadControl() { - long loadPositionUs; - if (isPendingReset()) { - loadPositionUs = pendingResetPositionUs; - } 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, pendingResetPositionUs, - 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; } } @@ -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) {