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 2ad1159c3e..93c0a7dc11 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 final class ExoPlayerTest extends TestCase {
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 faf86087c9..e4c109e85b 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 @@ import java.io.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 51663a21c6..102a689742 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 final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
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 97e9ddd7e7..31b76a84b3 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 @@ import java.io.IOException;
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 31ee8df1e4..3b06542855 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 @@ public interface MediaPeriod extends SequenceableLoader {
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 10c56e5576..077b5576c1 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 @@ import java.util.IdentityHashMap;
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 fd2ebffe8e..5b717e51da 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 @@ import java.util.Arrays;
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 1ae928045d..93d86a8de1 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 class ChunkSampleStream implements SampleStream, S
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 class ChunkSampleStream implements SampleStream, S
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 class ChunkSampleStream implements SampleStream, S
}
/**
- * 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 isPrimarySampleStream(SampleStream sampleStream) {
- return sampleStream instanceof ChunkSampleStream;
+ public void discardUnselectedEmbeddedTracksTo(long positionUs) {
+ for (int i = 0; i < embeddedSampleQueues.length; i++) {
+ if (!embeddedTracksSelected[i]) {
+ embeddedSampleQueues[i].skipToKeyframeBefore(positionUs, true);
+ }
+ }
}
/**
- * Returns whether a {@link SampleStream} is for an embedded track of a {@link ChunkSampleStream}.
+ * 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 static boolean isEmbeddedSampleStream(SampleStream sampleStream) {
- return sampleStream instanceof ChunkSampleStream.EmbeddedSampleStream;
- }
-
- /**
- * Returns the {@link SampleStream} for the embedded track with the specified type.
- */
- 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 class ChunkSampleStream implements SampleStream, S
}
// 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 class ChunkSampleStream implements SampleStream, S
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 class ChunkSampleStream implements SampleStream, S
*/
public void release() {
primarySampleQueue.disable();
- for (EmbeddedSampleStream embeddedSampleStream : embeddedSampleStreams) {
- embeddedSampleStream.disable();
+ for (DefaultTrackOutput embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.disable();
}
loader.release();
}
@@ -232,7 +245,6 @@ public class ChunkSampleStream implements SampleStream, S
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 class ChunkSampleStream implements SampleStream, S
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 class ChunkSampleStream implements SampleStream, S
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 @@ public class ChunkSampleStream implements SampleStream, S
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 class ChunkSampleStream implements SampleStream, S
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 8905607bc1..5e0541cb31 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.SequenceableLoader;
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 @@ import java.util.List;
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 @@ import java.util.List;
}
// 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 @@ import java.util.List;
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 @@ import java.util.List;
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 0ae8becfc0..23ccccbb6b 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 final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
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 b9af9930dc..43cd4a9f8d 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 @@ import java.util.ArrayList;
return positionUs;
}
+ @Override
+ public void discardBuffer(long positionUs) {
+ // Do nothing.
+ }
+
@Override
public boolean continueLoading(long positionUs) {
return sequenceableLoader.continueLoading(positionUs);