diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index d00b9a19a7..bf9f1f9ced 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -150,7 +150,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { joiningDeadlineMs = C.TIME_UNSET; clearReportedVideoSize(); formatHolder = new FormatHolder(); - flagsOnlyBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); eventDispatcher = new EventDispatcher(eventHandler, eventListener); outputMode = VpxDecoder.OUTPUT_MODE_NONE; } @@ -185,6 +185,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } + // We have a format. if (isRendererAvailable()) { drmSession = pendingDrmSession; ExoMediaCrypto mediaCrypto = null; @@ -222,7 +223,20 @@ public final class LibvpxVideoRenderer extends BaseRenderer { throw ExoPlaybackException.createForRenderer(e, getIndex()); } } else { - skipToKeyframeBefore(positionUs); + skipSource(positionUs); + // We need to read any format changes despite not having a codec so that drmSession can be + // updated, and so that we have the most recent format should the codec be initialized. We may + // also reach the end of the stream. Note that readSource will not read a sample into a + // flags-only buffer. + flagsOnlyBuffer.clear(); + int result = readSource(formatHolder, flagsOnlyBuffer, false); + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder.format); + } else if (result == C.RESULT_BUFFER_READ) { + Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); + inputStreamEnded = true; + outputStreamEnded = true; + } } decoderCounters.ensureUpdated(); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 9fc55d5c77..434a0249df 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -551,7 +551,7 @@ public final class ExoPlayerTest extends TestCase { } @Override - public void skipToKeyframeBefore(long timeUs) { + public void skipData(long positionUs) { // Do nothing. } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index f6aae200dd..8324b184ca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -296,6 +296,16 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return result; } + /** + * Attempts to skip to the keyframe before the specified position, or to the end of the stream if + * {@code positionUs} is beyond it. + * + * @param positionUs The position in microseconds. + */ + protected void skipSource(long positionUs) { + stream.skipData(positionUs - streamOffsetUs); + } + /** * Returns whether the upstream source is ready. * @@ -305,13 +315,4 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return readEndOfStream ? streamIsFinal : stream.isReady(); } - /** - * Attempts to skip to the keyframe before the specified time. - * - * @param timeUs The specified time. - */ - protected void skipToKeyframeBefore(long timeUs) { - stream.skipToKeyframeBefore(timeUs - streamOffsetUs); - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 3a50a8244a..ddb870f6ff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -146,7 +146,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements eventDispatcher = new EventDispatcher(eventHandler, eventListener); audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener()); formatHolder = new FormatHolder(); - flagsOnlyBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); decoderReinitializationState = REINITIALIZATION_STATE_NONE; audioTrackNeedsConfigure = true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java index 84c89de427..d22a45ce88 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java @@ -63,6 +63,15 @@ public class DecoderInputBuffer extends Buffer { @BufferReplacementMode private final int bufferReplacementMode; + /** + * Creates a new instance for which {@link #isFlagsOnly()} will return true. + * + * @return A new flags only input buffer. + */ + public static DecoderInputBuffer newFlagsOnlyInstance() { + return new DecoderInputBuffer(BUFFER_REPLACEMENT_MODE_DISABLED); + } + /** * @param bufferReplacementMode Determines the behavior of {@link #ensureSpaceForWrite(int)}. One * of {@link #BUFFER_REPLACEMENT_MODE_DISABLED}, {@link #BUFFER_REPLACEMENT_MODE_NORMAL} and @@ -109,6 +118,14 @@ public class DecoderInputBuffer extends Buffer { data = newData; } + /** + * Returns whether the buffer is only able to hold flags, meaning {@link #data} is null and + * its replacement mode is {@link #BUFFER_REPLACEMENT_MODE_DISABLED}. + */ + public final boolean isFlagsOnly() { + return data == null && bufferReplacementMode == BUFFER_REPLACEMENT_MODE_DISABLED; + } + /** * Returns whether the {@link C#BUFFER_FLAG_ENCRYPTED} flag is set. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java index d57b55bfd2..1c9a148226 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java @@ -76,7 +76,6 @@ public final class DefaultTrackOutput implements TrackOutput { private long totalBytesWritten; private Allocation lastAllocation; private int lastAllocationOffset; - private boolean needKeyframe; private boolean pendingSplice; private UpstreamFormatChangedListener upstreamFormatChangeListener; @@ -92,7 +91,6 @@ public final class DefaultTrackOutput implements TrackOutput { scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); state = new AtomicInteger(); lastAllocationOffset = allocationLength; - needKeyframe = true; } // Called by the consuming thread, but only when there is no loading thread. @@ -227,6 +225,16 @@ public final class DefaultTrackOutput implements TrackOutput { return infoQueue.getLargestQueuedTimestampUs(); } + /** + * Skips all samples currently in the buffer. + */ + public void skipAll() { + long nextOffset = infoQueue.skipAll(); + if (nextOffset != C.POSITION_UNSET) { + dropDownstreamTo(nextOffset); + } + } + /** * Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer * contains a keyframe with a timestamp of {@code timeUs} or earlier. If @@ -523,12 +531,6 @@ public final class DefaultTrackOutput implements TrackOutput { } pendingSplice = false; } - if (needKeyframe) { - if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0) { - return; - } - needKeyframe = false; - } timeUs += sampleOffsetUs; long absoluteOffset = totalBytesWritten - size - offset; infoQueue.commitSample(timeUs, flags, absoluteOffset, size, encryptionKey); @@ -558,7 +560,6 @@ public final class DefaultTrackOutput implements TrackOutput { totalBytesWritten = 0; lastAllocation = null; lastAllocationOffset = allocationLength; - needKeyframe = true; } /** @@ -615,6 +616,7 @@ public final class DefaultTrackOutput implements TrackOutput { private long largestDequeuedTimestampUs; private long largestQueuedTimestampUs; + private boolean upstreamKeyframeRequired; private boolean upstreamFormatRequired; private Format upstreamFormat; private int upstreamSourceId; @@ -631,6 +633,7 @@ public final class DefaultTrackOutput implements TrackOutput { largestDequeuedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE; upstreamFormatRequired = true; + upstreamKeyframeRequired = true; } public void clearSampleData() { @@ -638,6 +641,7 @@ public final class DefaultTrackOutput implements TrackOutput { relativeReadIndex = 0; relativeWriteIndex = 0; queueSize = 0; + upstreamKeyframeRequired = true; } // Called by the consuming thread, but only when there is no loading thread. @@ -780,6 +784,10 @@ public final class DefaultTrackOutput implements TrackOutput { return C.RESULT_FORMAT_READ; } + if (buffer.isFlagsOnly()) { + return C.RESULT_NOTHING_READ; + } + buffer.timeUs = timesUs[relativeReadIndex]; buffer.setFlags(flags[relativeReadIndex]); extrasHolder.size = sizes[relativeReadIndex]; @@ -800,6 +808,24 @@ public final class DefaultTrackOutput implements TrackOutput { return C.RESULT_BUFFER_READ; } + /** + * Skips all samples in the buffer. + * + * @return The offset up to which data should be dropped, or {@link C#POSITION_UNSET} if no + * dropping of data is required. + */ + public synchronized long skipAll() { + if (queueSize == 0) { + return C.POSITION_UNSET; + } + + int lastSampleIndex = (relativeReadIndex + queueSize - 1) % capacity; + relativeReadIndex = (relativeReadIndex + queueSize) % capacity; + absoluteReadIndex += queueSize; + queueSize = 0; + return offsets[lastSampleIndex] + sizes[lastSampleIndex]; + } + /** * Attempts to locate the keyframe before or at the specified time. If * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs} @@ -842,9 +868,9 @@ public final class DefaultTrackOutput implements TrackOutput { return C.POSITION_UNSET; } - queueSize -= sampleCountToKeyframe; relativeReadIndex = (relativeReadIndex + sampleCountToKeyframe) % capacity; absoluteReadIndex += sampleCountToKeyframe; + queueSize -= sampleCountToKeyframe; return offsets[relativeReadIndex]; } @@ -867,6 +893,12 @@ public final class DefaultTrackOutput implements TrackOutput { public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset, int size, byte[] encryptionKey) { + if (upstreamKeyframeRequired) { + if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) { + return; + } + upstreamKeyframeRequired = false; + } Assertions.checkState(!upstreamFormatRequired); commitSampleTimestamp(timeUs); timesUs[relativeWriteIndex] = timeUs; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 3fbbfac652..8af024f666 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -169,6 +169,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private final DrmSessionManager drmSessionManager; private final boolean playClearSamplesWithoutKeys; private final DecoderInputBuffer buffer; + private final DecoderInputBuffer flagsOnlyBuffer; private final FormatHolder formatHolder; private final List decodeOnlyPresentationTimestamps; private final MediaCodec.BufferInfo outputBufferInfo; @@ -227,6 +228,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { this.drmSessionManager = drmSessionManager; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); formatHolder = new FormatHolder(); decodeOnlyPresentationTimestamps = new ArrayList<>(); outputBufferInfo = new MediaCodec.BufferInfo(); @@ -448,6 +450,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecReconfigurationState = RECONFIGURATION_STATE_NONE; codecReinitializationState = REINITIALIZATION_STATE_NONE; decoderCounters.decoderReleaseCount++; + buffer.data = null; try { codec.stop(); } finally { @@ -486,12 +489,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (format == null) { // We don't have a format yet, so try and read one. buffer.clear(); - int result = readSource(formatHolder, buffer, true); + int result = readSource(formatHolder, flagsOnlyBuffer, true); if (result == C.RESULT_FORMAT_READ) { onInputFormatChanged(formatHolder.format); } else if (result == C.RESULT_BUFFER_READ) { // End of stream read having not read a format. - Assertions.checkState(buffer.isEndOfStream()); + Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); inputStreamEnded = true; processEndOfStream(); return; @@ -500,14 +503,28 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return; } } + // We have a format. maybeInitCodec(); if (codec != null) { TraceUtil.beginSection("drainAndFeed"); while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} while (feedInputBuffer()) {} TraceUtil.endSection(); - } else if (format != null) { - skipToKeyframeBefore(positionUs); + } else { + skipSource(positionUs); + // We need to read any format changes despite not having a codec so that drmSession can be + // updated, and so that we have the most recent format should the codec be initialized. We may + // also reach the end of the stream. Note that readSource will not read a sample into a + // flags-only buffer. + flagsOnlyBuffer.clear(); + int result = readSource(formatHolder, flagsOnlyBuffer, false); + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder.format); + } else if (result == C.RESULT_BUFFER_READ) { + Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); + inputStreamEnded = true; + processEndOfStream(); + } } decoderCounters.ensureUpdated(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 102a689742..e14930c7b8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -262,8 +262,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } @Override - public void skipToKeyframeBefore(long timeUs) { - stream.skipToKeyframeBefore(startUs + timeUs); + public void skipData(long positionUs) { + stream.skipData(startUs + positionUs); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java index eb94351f61..7aab22d8a0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java @@ -43,7 +43,7 @@ public final class EmptySampleStream implements SampleStream { } @Override - public void skipToKeyframeBefore(long timeUs) { + public void skipData(long positionUs) { // Do nothing. } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index d54e881183..f247d4dd37 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -340,8 +340,13 @@ import java.io.IOException; loadingFinished, lastSeekPositionUs); } - /* package */ void skipToKeyframeBefore(int track, long timeUs) { - sampleQueues.valueAt(track).skipToKeyframeBefore(timeUs, true); + /* package */ void skipData(int track, long positionUs) { + DefaultTrackOutput sampleQueue = sampleQueues.valueAt(track); + if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { + sampleQueue.skipAll(); + } else { + sampleQueue.skipToKeyframeBefore(positionUs, true); + } } // Loader.Callback implementation. @@ -569,8 +574,8 @@ import java.io.IOException; } @Override - public void skipToKeyframeBefore(long timeUs) { - ExtractorMediaPeriod.this.skipToKeyframeBefore(track, timeUs); + public void skipData(long positionUs) { + ExtractorMediaPeriod.this.skipData(track, positionUs); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java index e3039878f8..dc58c29c22 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java @@ -66,10 +66,11 @@ public interface SampleStream { int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired); /** - * Attempts to skip to the keyframe before the specified time. + * Attempts to skip to the keyframe before the specified position, or to the end of the stream if + * {@code positionUs} is beyond it. * - * @param timeUs The specified time. + * @param positionUs The specified time. */ - void skipToKeyframeBefore(long timeUs); + void skipData(long positionUs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index 5b717e51da..8e38588e89 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -235,8 +235,10 @@ import java.util.Arrays; } @Override - public void skipToKeyframeBefore(long timeUs) { - // Do nothing. + public void skipData(long positionUs) { + if (positionUs > 0) { + streamState = STREAM_STATE_END_OF_STREAM; + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 6a62afe2fb..c43f3d577a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -251,8 +251,12 @@ public class ChunkSampleStream implements SampleStream, S } @Override - public void skipToKeyframeBefore(long timeUs) { - primarySampleQueue.skipToKeyframeBefore(timeUs, true); + public void skipData(long positionUs) { + if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) { + primarySampleQueue.skipAll(); + } else { + primarySampleQueue.skipToKeyframeBefore(positionUs, true); + } } // Loader.Callback implementation. @@ -448,8 +452,12 @@ public class ChunkSampleStream implements SampleStream, S } @Override - public void skipToKeyframeBefore(long timeUs) { - sampleQueue.skipToKeyframeBefore(timeUs, true); + public void skipData(long positionUs) { + if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { + sampleQueue.skipAll(); + } else { + sampleQueue.skipToKeyframeBefore(positionUs, true); + } } @Override diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index d8eb7e1ae8..450644f60f 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -50,8 +50,8 @@ import java.io.IOException; } @Override - public void skipToKeyframeBefore(long timeUs) { - sampleStreamWrapper.skipToKeyframeBefore(group, timeUs); + public void skipData(long positionUs) { + sampleStreamWrapper.skipData(group, positionUs); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index f2cf7d2ddb..827a6e885d 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -312,8 +312,13 @@ import java.util.LinkedList; loadingFinished, lastSeekPositionUs); } - /* package */ void skipToKeyframeBefore(int group, long timeUs) { - sampleQueues.valueAt(group).skipToKeyframeBefore(timeUs, true); + /* package */ void skipData(int group, long positionUs) { + DefaultTrackOutput sampleQueue = sampleQueues.valueAt(group); + if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { + sampleQueue.skipAll(); + } else { + sampleQueue.skipToKeyframeBefore(positionUs, true); + } } private boolean finishedReadingChunk(HlsMediaChunk chunk) {