diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 2ad1159c3ec..93c0a7dc116 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -460,6 +460,11 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF return 0; } + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + @Override public long readDiscontinuity() { assertTrue(preparedPeriod); diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index faf86087c93..e4c109e85bc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -455,6 +455,8 @@ private void doSomeWork() throws ExoPlaybackException, IOException { TraceUtil.beginSection("doSomeWork"); updatePlaybackPositions(); + playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs); + boolean allRenderersEnded = true; boolean allRenderersReadyOrEnded = true; for (Renderer renderer : enabledRenderers) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 51663a21c68..102a6897428 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -109,6 +109,11 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF return enablePositionUs - startUs; } + @Override + public void discardBuffer(long positionUs) { + mediaPeriod.discardBuffer(positionUs + startUs); + } + @Override public long readDiscontinuity() { if (pendingInitialDiscontinuity) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 97e9ddd7e76..31b76a84b36 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -234,6 +234,11 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF return positionUs; } + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long playbackPositionUs) { if (loadingFinished || (prepared && enabledTrackCount == 0)) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index 31ee8df1e43..3b065428558 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -104,6 +104,13 @@ interface Callback extends SequenceableLoader.Callback { long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs); + /** + * Discards buffered media up to the specified position. + * + * @param positionUs The position in microseconds. + */ + void discardBuffer(long positionUs); + /** * Attempts to read a discontinuity. *

diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index 10c56e55762..077b5576c19 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -128,6 +128,13 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF return positionUs; } + @Override + public void discardBuffer(long positionUs) { + for (MediaPeriod period : enabledPeriods) { + period.discardBuffer(positionUs); + } + } + @Override public boolean continueLoading(long positionUs) { return sequenceableLoader.continueLoading(positionUs); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index fd2ebffe8e4..5b717e51dac 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -111,6 +111,11 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF return positionUs; } + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long positionUs) { if (loadingFinished || loader.isLoading()) { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 1ae928045da..93d86a8de19 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -40,6 +40,7 @@ public class ChunkSampleStream implements SampleStream, S private final int primaryTrackType; private final int[] embeddedTrackTypes; + private final boolean[] embeddedTracksSelected; private final T chunkSource; private final SequenceableLoader.Callback> callback; private final EventDispatcher eventDispatcher; @@ -49,7 +50,7 @@ public class ChunkSampleStream implements SampleStream, S private final LinkedList mediaChunks; private final List readOnlyMediaChunks; private final DefaultTrackOutput primarySampleQueue; - private final EmbeddedSampleStream[] embeddedSampleStreams; + private final DefaultTrackOutput[] embeddedSampleQueues; private final BaseMediaChunkOutput mediaChunkOutput; private Format primaryDownstreamTrackFormat; @@ -84,7 +85,8 @@ public ChunkSampleStream(int primaryTrackType, int[] embeddedTrackTypes, T chunk readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; - embeddedSampleStreams = newEmbeddedSampleStreamArray(embeddedTrackCount); + embeddedSampleQueues = new DefaultTrackOutput[embeddedTrackCount]; + embeddedTracksSelected = new boolean[embeddedTrackCount]; int[] trackTypes = new int[1 + embeddedTrackCount]; DefaultTrackOutput[] sampleQueues = new DefaultTrackOutput[1 + embeddedTrackCount]; @@ -93,9 +95,10 @@ public ChunkSampleStream(int primaryTrackType, int[] embeddedTrackTypes, T chunk sampleQueues[0] = primarySampleQueue; for (int i = 0; i < embeddedTrackCount; i++) { + DefaultTrackOutput trackOutput = new DefaultTrackOutput(allocator); + embeddedSampleQueues[i] = trackOutput; + sampleQueues[i + 1] = trackOutput; trackTypes[i + 1] = embeddedTrackTypes[i]; - sampleQueues[i + 1] = new DefaultTrackOutput(allocator); - embeddedSampleStreams[i] = new EmbeddedSampleStream(sampleQueues[i + 1]); } mediaChunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues); @@ -104,26 +107,36 @@ public ChunkSampleStream(int primaryTrackType, int[] embeddedTrackTypes, T chunk } /** - * Returns whether a {@link SampleStream} is for an embedded track of a {@link ChunkSampleStream}. - */ - public static boolean isPrimarySampleStream(SampleStream sampleStream) { - return sampleStream instanceof ChunkSampleStream; - } - - /** - * Returns whether a {@link SampleStream} is for an embedded track of a {@link ChunkSampleStream}. + * Discards buffered media for embedded tracks that are not currently selected, up to the + * specified position. + * + * @param positionUs The position to discard up to, in microseconds. */ - public static boolean isEmbeddedSampleStream(SampleStream sampleStream) { - return sampleStream instanceof ChunkSampleStream.EmbeddedSampleStream; + public void discardUnselectedEmbeddedTracksTo(long positionUs) { + for (int i = 0; i < embeddedSampleQueues.length; i++) { + if (!embeddedTracksSelected[i]) { + embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true); + } + } } /** - * Returns the {@link SampleStream} for the embedded track with the specified type. + * Selects the embedded track, returning a new {@link EmbeddedSampleStream} from which the track's + * samples can be consumed. {@link EmbeddedSampleStream#release()} must be called on the returned + * stream when the track is no longer required, and before calling this method again to obtain + * another stream for the same track. + * + * @param positionUs The current playback position in microseconds. + * @param trackType The type of the embedded track to enable. + * @return The {@link EmbeddedSampleStream} for the embedded track. */ - public SampleStream getEmbeddedSampleStream(int trackType) { - for (int i = 0; i < embeddedTrackTypes.length; i++) { + public EmbeddedSampleStream selectEmbeddedTrack(long positionUs, int trackType) { + for (int i = 0; i < embeddedSampleQueues.length; i++) { if (embeddedTrackTypes[i] == trackType) { - return embeddedSampleStreams[i]; + Assertions.checkState(!embeddedTracksSelected[i]); + embeddedTracksSelected[i] = true; + embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true); + return new EmbeddedSampleStream(this, embeddedSampleQueues[i], i); } } // Should never happen. @@ -179,8 +192,8 @@ public void seekToUs(long positionUs) { } // TODO: For this to work correctly, the embedded streams must not discard anything from their // sample queues beyond the current read position of the primary stream. - for (EmbeddedSampleStream embeddedSampleStream : embeddedSampleStreams) { - embeddedSampleStream.skipToKeyframeBefore(positionUs); + for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.skipToKeyframeBefore(positionUs); } } else { // We failed, and need to restart. @@ -191,8 +204,8 @@ public void seekToUs(long positionUs) { loader.cancelLoading(); } else { primarySampleQueue.reset(true); - for (EmbeddedSampleStream embeddedSampleStream : embeddedSampleStreams) { - embeddedSampleStream.reset(true); + for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.reset(true); } } } @@ -205,8 +218,8 @@ public void seekToUs(long positionUs) { */ public void release() { primarySampleQueue.disable(); - for (EmbeddedSampleStream embeddedSampleStream : embeddedSampleStreams) { - embeddedSampleStream.disable(); + for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.disable(); } loader.release(); } @@ -232,7 +245,6 @@ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, if (isPendingReset()) { return C.RESULT_NOTHING_READ; } - // TODO: For embedded streams that aren't being used, we need to drain their queues here. discardDownstreamMediaChunks(primarySampleQueue.getReadIndex()); return primarySampleQueue.readData(formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); @@ -264,8 +276,8 @@ public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDura loadable.bytesLoaded()); if (!released) { primarySampleQueue.reset(true); - for (EmbeddedSampleStream embeddedStream : embeddedSampleStreams) { - embeddedStream.reset(true); + for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.reset(true); } callback.onContinueLoadingRequested(this); } @@ -284,8 +296,8 @@ public int onLoadError(Chunk loadable, long elapsedRealtimeMs, long loadDuration BaseMediaChunk removed = mediaChunks.removeLast(); Assertions.checkState(removed == loadable); primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); - for (int i = 0; i < embeddedSampleStreams.length; i++) { - embeddedSampleStreams[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); + for (int i = 0; i < embeddedSampleQueues.length; i++) { + embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); } if (mediaChunks.isEmpty()) { pendingResetPositionUs = lastSeekPositionUs; @@ -406,25 +418,28 @@ private boolean discardUpstreamMediaChunks(int queueLength) { loadingFinished = false; } primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); - for (int i = 0; i < embeddedSampleStreams.length; i++) { - embeddedSampleStreams[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); + for (int i = 0; i < embeddedSampleQueues.length; i++) { + embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); } eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs); return true; } - @SuppressWarnings("unchecked") - private static ChunkSampleStream.EmbeddedSampleStream[] - newEmbeddedSampleStreamArray(int length) { - return new ChunkSampleStream.EmbeddedSampleStream[length]; - } + /** + * A {@link SampleStream} embedded in a {@link ChunkSampleStream}. + */ + public final class EmbeddedSampleStream implements SampleStream { - private final class EmbeddedSampleStream implements SampleStream { + public final ChunkSampleStream parent; private final DefaultTrackOutput sampleQueue; + private final int index; - public EmbeddedSampleStream(DefaultTrackOutput sampleQueue) { + public EmbeddedSampleStream(ChunkSampleStream parent, DefaultTrackOutput sampleQueue, + int index) { + this.parent = parent; this.sampleQueue = sampleQueue; + this.index = index; } @Override @@ -452,16 +467,9 @@ public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, lastSeekPositionUs); } - public void reset(boolean enable) { - sampleQueue.reset(enable); - } - - public void disable() { - sampleQueue.disable(); - } - - public void discardUpstreamSamples(int discardFromIndex) { - sampleQueue.discardUpstreamSamples(discardFromIndex); + public void release() { + Assertions.checkState(embeddedTracksSelected[index]); + embeddedTracksSelected[index] = false; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 8905607bc19..5e0541cb31d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.ChunkSampleStream; +import com.google.android.exoplayer2.source.chunk.ChunkSampleStream.EmbeddedSampleStream; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.Representation; @@ -125,7 +126,7 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF HashMap> primarySampleStreams = new HashMap<>(); // First pass for primary tracks. for (int i = 0; i < selections.length; i++) { - if (ChunkSampleStream.isPrimarySampleStream(streams[i])) { + if (streams[i] instanceof ChunkSampleStream) { @SuppressWarnings("unchecked") ChunkSampleStream stream = (ChunkSampleStream) streams[i]; if (selections[i] == null || !mayRetainStreamFlags[i]) { @@ -149,26 +150,31 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF } // Second pass for embedded tracks. for (int i = 0; i < selections.length; i++) { - if (ChunkSampleStream.isEmbeddedSampleStream(streams[i])) { - // Always clear even if the selection is unchanged, since the parent primary sample stream - // may have been replaced. + if ((streams[i] instanceof EmbeddedSampleStream || streams[i] instanceof EmptySampleStream) + && (selections[i] == null || !mayRetainStreamFlags[i])) { + // The stream is for an embedded track and is either no longer selected or needs replacing. + releaseIfEmbeddedSampleStream(streams[i]); streams[i] = null; } - if (streams[i] == null && selections[i] != null) { + // We need to consider replacing the stream even if it's non-null because the primary stream + // may have been replaced, selected or deselected. + if (selections[i] != null) { int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); if (trackGroupIndex >= adaptationSetCount) { - EmbeddedTrackInfo embeddedTrackInfo = - embeddedTrackInfos[trackGroupIndex - adaptationSetCount]; + int embeddedTrackIndex = trackGroupIndex - adaptationSetCount; + EmbeddedTrackInfo embeddedTrackInfo = embeddedTrackInfos[embeddedTrackIndex]; int adaptationSetIndex = embeddedTrackInfo.adaptationSetIndex; - ChunkSampleStream primarySampleStream = - primarySampleStreams.get(adaptationSetIndex); - if (primarySampleStream != null) { - streams[i] = primarySampleStream.getEmbeddedSampleStream(embeddedTrackInfo.trackType); - } else { - // The primary track in which this one is embedded is not selected. - streams[i] = new EmptySampleStream(); + ChunkSampleStream primaryStream = primarySampleStreams.get(adaptationSetIndex); + SampleStream stream = streams[i]; + boolean mayRetainStream = primaryStream == null ? stream instanceof EmptySampleStream + : (stream instanceof EmbeddedSampleStream + && ((EmbeddedSampleStream) stream).parent == primaryStream); + if (!mayRetainStream) { + releaseIfEmbeddedSampleStream(stream); + streams[i] = primaryStream == null ? new EmptySampleStream() + : primaryStream.selectEmbeddedTrack(positionUs, embeddedTrackInfo.trackType); + streamResetFlags[i] = true; } - streamResetFlags[i] = true; } } } @@ -178,6 +184,13 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF return positionUs; } + @Override + public void discardBuffer(long positionUs) { + for (ChunkSampleStream sampleStream : sampleStreams) { + sampleStream.discardUnselectedEmbeddedTracksTo(positionUs); + } + } + @Override public boolean continueLoading(long positionUs) { return sequenceableLoader.continueLoading(positionUs); @@ -321,6 +334,12 @@ private static ChunkSampleStream[] newSampleStreamArray(int len return new ChunkSampleStream[length]; } + private static void releaseIfEmbeddedSampleStream(SampleStream sampleStream) { + if (sampleStream instanceof EmbeddedSampleStream) { + ((EmbeddedSampleStream) sampleStream).release(); + } + } + private static final class EmbeddedTrackInfo { public final int adaptationSetIndex; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 0ae8becfc05..23ccccbb6b0 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -189,6 +189,11 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF return positionUs; } + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long positionUs) { return sequenceableLoader.continueLoading(positionUs); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index b9af9930dc6..43cd4a9f8d4 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -136,6 +136,11 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF return positionUs; } + @Override + public void discardBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long positionUs) { return sequenceableLoader.continueLoading(positionUs);