diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java index d1b5ce600d..e19de76466 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java @@ -68,7 +68,7 @@ public class AdtsReaderTest extends TestCase { @Override protected void setUp() throws Exception { - FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(true); + FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); adtsOutput = fakeExtractorOutput.track(0); id3Output = fakeExtractorOutput.track(1); adtsReader = new AdtsReader(true); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java index 6bf65710fc..4120110afb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java @@ -56,7 +56,7 @@ public interface Extractor { boolean sniff(ExtractorInput input) throws IOException, InterruptedException; /** - * Initializes the extractor with an {@link ExtractorOutput}. + * Initializes the extractor with an {@link ExtractorOutput}. Called at most once. * * @param output An {@link ExtractorOutput} to receive extracted data. */ diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java index d138c7ce3a..a547f745ca 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java @@ -21,18 +21,19 @@ package com.google.android.exoplayer2.extractor; public interface ExtractorOutput { /** - * Called when the {@link Extractor} identifies the existence of a track in the stream. + * Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track. *

- * Returns a {@link TrackOutput} that will receive track level data belonging to the track. + * The same {@link TrackOutput} is returned if multiple calls are made with the same + * {@code trackId}. * - * @param trackId A unique track identifier. - * @return The {@link TrackOutput} that should receive track level data belonging to the track. + * @param trackId A track identifier. + * @return The {@link TrackOutput} for the given track identifier. */ TrackOutput track(int trackId); /** - * Called when all tracks have been identified, meaning that {@link #track(int)} will not be - * called again. + * Called when all tracks have been identified, meaning no new {@code trackId} values will be + * passed to {@link #track(int)}. */ void endTracks(); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index f5a7d090e1..bac362d711 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -54,7 +54,6 @@ public final class TsExtractor implements Extractor { }; - public static final int TS_STREAM_TYPE_MPA = 0x03; public static final int TS_STREAM_TYPE_MPA_LSF = 0x04; public static final int TS_STREAM_TYPE_AAC = 0x0F; @@ -92,6 +91,7 @@ public final class TsExtractor implements Extractor { // Accessed only by the loading thread. private ExtractorOutput output; + private boolean tracksEnded; private ElementaryStreamReader id3Reader; public TsExtractor() { @@ -120,8 +120,8 @@ public final class TsExtractor implements Extractor { tsScratch = new ParsableBitArray(new byte[3]); trackIds = new SparseBooleanArray(); tsPayloadReaders = new SparseArray<>(); - tsPayloadReaders.put(TS_PAT_PID, new PatReader()); continuityCounters = new SparseIntArray(); + resetPayloadReaders(); } // Extractor implementation. @@ -153,11 +153,10 @@ public final class TsExtractor implements Extractor { @Override public void seek(long position) { timestampAdjuster.reset(); - for (int i = 0; i < tsPayloadReaders.size(); i++) { - tsPayloadReaders.valueAt(i).seek(); - } tsPacketBuffer.reset(); continuityCounters.clear(); + // Elementary stream readers' state should be cleared to get consistent behaviours when seeking. + resetPayloadReaders(); } @Override @@ -252,6 +251,13 @@ public final class TsExtractor implements Extractor { // Internals. + private void resetPayloadReaders() { + trackIds.clear(); + tsPayloadReaders.clear(); + tsPayloadReaders.put(TS_PAT_PID, new PatReader()); + id3Reader = null; + } + /** * Parses TS packet payload data. */ @@ -345,7 +351,7 @@ public final class TsExtractor implements Extractor { patScratch.skipBits(13); // network_PID (13) } else { int pid = patScratch.readBits(13); - tsPayloadReaders.put(pid, new PmtReader()); + tsPayloadReaders.put(pid, new PmtReader(pid)); } } } @@ -365,14 +371,16 @@ public final class TsExtractor implements Extractor { private final ParsableBitArray pmtScratch; private final ParsableByteArray sectionData; + private final int pid; private int sectionLength; private int sectionBytesRead; private int crc; - public PmtReader() { + public PmtReader(int pid) { pmtScratch = new ParsableBitArray(new byte[5]); sectionData = new ParsableByteArray(); + this.pid = pid; } @Override @@ -466,8 +474,16 @@ public final class TsExtractor implements Extractor { tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader, timestampAdjuster)); } } - - output.endTracks(); + if (mapByType) { + if (!tracksEnded) { + output.endTracks(); + } + } else { + tsPayloadReaders.remove(TS_PAT_PID); + tsPayloadReaders.remove(pid); + output.endTracks(); + } + tracksEnded = true; } /** 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 49f2cffca5..27bd1f677f 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; +import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; @@ -41,7 +42,6 @@ import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; -import java.util.Arrays; /** * A {@link MediaPeriod} that extracts data using an {@link Extractor}. @@ -68,6 +68,7 @@ import java.util.Arrays; private final Runnable maybeFinishPrepareRunnable; private final Runnable onContinueLoadingRequestedRunnable; private final Handler handler; + private final SparseArray sampleQueues; private Callback callback; private SeekMap seekMap; @@ -77,7 +78,6 @@ import java.util.Arrays; private boolean seenFirstTrackSelection; private boolean notifyReset; private int enabledTrackCount; - private DefaultTrackOutput[] sampleQueues; private TrackGroupArray tracks; private long durationUs; private boolean[] trackEnabledStates; @@ -131,7 +131,7 @@ import java.util.Arrays; handler = new Handler(); pendingResetPositionUs = C.TIME_UNSET; - sampleQueues = new DefaultTrackOutput[0]; + sampleQueues = new SparseArray<>(); length = C.LENGTH_UNSET; } @@ -141,8 +141,9 @@ import java.util.Arrays; @Override public void run() { extractorHolder.release(); - for (DefaultTrackOutput sampleQueue : sampleQueues) { - sampleQueue.disable(); + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { + sampleQueues.valueAt(i).disable(); } } }); @@ -178,7 +179,7 @@ import java.util.Arrays; Assertions.checkState(trackEnabledStates[track]); enabledTrackCount--; trackEnabledStates[track] = false; - sampleQueues[track].disable(); + sampleQueues.valueAt(track).disable(); streams[i] = null; } } @@ -201,9 +202,10 @@ import java.util.Arrays; if (!seenFirstTrackSelection) { // At the time of the first track selection all queues will be enabled, so we need to disable // any that are no longer required. - for (int i = 0; i < sampleQueues.length; i++) { + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { if (!trackEnabledStates[i]) { - sampleQueues[i].disable(); + sampleQueues.valueAt(i).disable(); } } } @@ -270,11 +272,12 @@ import java.util.Arrays; // Treat all seeks into non-seekable media as being to t=0. positionUs = seekMap.isSeekable() ? positionUs : 0; lastSeekPositionUs = positionUs; + int trackCount = sampleQueues.size(); // If we're not pending a reset, see if we can seek within the sample queues. boolean seekInsideBuffer = !isPendingReset(); - for (int i = 0; seekInsideBuffer && i < sampleQueues.length; i++) { + for (int i = 0; seekInsideBuffer && i < trackCount; i++) { if (trackEnabledStates[i]) { - seekInsideBuffer = sampleQueues[i].skipToKeyframeBefore(positionUs); + seekInsideBuffer = sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs); } } // If we failed to seek within the sample queues, we need to restart. @@ -284,8 +287,8 @@ import java.util.Arrays; if (loader.isLoading()) { loader.cancelLoading(); } else { - for (int i = 0; i < sampleQueues.length; i++) { - sampleQueues[i].reset(trackEnabledStates[i]); + for (int i = 0; i < trackCount; i++) { + sampleQueues.valueAt(i).reset(trackEnabledStates[i]); } } } @@ -296,7 +299,7 @@ import java.util.Arrays; // SampleStream methods. /* package */ boolean isReady(int track) { - return loadingFinished || (!isPendingReset() && !sampleQueues[track].isEmpty()); + return loadingFinished || (!isPendingReset() && !sampleQueues.valueAt(track).isEmpty()); } /* package */ void maybeThrowError() throws IOException { @@ -308,7 +311,8 @@ import java.util.Arrays; return C.RESULT_NOTHING_READ; } - return sampleQueues[track].readData(formatHolder, buffer, loadingFinished, lastSeekPositionUs); + return sampleQueues.valueAt(track).readData(formatHolder, buffer, loadingFinished, + lastSeekPositionUs); } // Loader.Callback implementation. @@ -332,8 +336,9 @@ import java.util.Arrays; long loadDurationMs, boolean released) { copyLengthFromLoader(loadable); if (!released && enabledTrackCount > 0) { - for (int i = 0; i < sampleQueues.length; i++) { - sampleQueues[i].reset(trackEnabledStates[i]); + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { + sampleQueues.valueAt(i).reset(trackEnabledStates[i]); } callback.onContinueLoadingRequested(this); } @@ -358,11 +363,13 @@ import java.util.Arrays; @Override public TrackOutput track(int id) { - sampleQueues = Arrays.copyOf(sampleQueues, sampleQueues.length + 1); - DefaultTrackOutput sampleQueue = new DefaultTrackOutput(allocator); - sampleQueue.setUpstreamFormatChangeListener(this); - sampleQueues[sampleQueues.length - 1] = sampleQueue; - return sampleQueue; + DefaultTrackOutput trackOutput = sampleQueues.get(id); + if (trackOutput == null) { + trackOutput = new DefaultTrackOutput(allocator); + trackOutput.setUpstreamFormatChangeListener(this); + sampleQueues.put(id, trackOutput); + } + return trackOutput; } @Override @@ -390,18 +397,18 @@ import java.util.Arrays; if (released || prepared || seekMap == null || !tracksBuilt) { return; } - for (DefaultTrackOutput sampleQueue : sampleQueues) { - if (sampleQueue.getUpstreamFormat() == null) { + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { + if (sampleQueues.valueAt(i).getUpstreamFormat() == null) { return; } } loadCondition.close(); - int trackCount = sampleQueues.length; TrackGroup[] trackArray = new TrackGroup[trackCount]; trackEnabledStates = new boolean[trackCount]; durationUs = seekMap.getDurationUs(); for (int i = 0; i < trackCount; i++) { - trackArray[i] = new TrackGroup(sampleQueues[i].getUpstreamFormat()); + trackArray[i] = new TrackGroup(sampleQueues.valueAt(i).getUpstreamFormat()); } tracks = new TrackGroupArray(trackArray); prepared = true; @@ -455,8 +462,9 @@ import java.util.Arrays; // a new load. lastSeekPositionUs = 0; notifyReset = prepared; - for (int i = 0; i < sampleQueues.length; i++) { - sampleQueues[i].reset(!prepared || trackEnabledStates[i]); + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { + sampleQueues.valueAt(i).reset(!prepared || trackEnabledStates[i]); } loadable.setLoadPosition(0); } @@ -464,17 +472,19 @@ import java.util.Arrays; private int getExtractedSamplesCount() { int extractedSamplesCount = 0; - for (DefaultTrackOutput sampleQueue : sampleQueues) { - extractedSamplesCount += sampleQueue.getWriteIndex(); + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { + extractedSamplesCount += sampleQueues.valueAt(i).getWriteIndex(); } return extractedSamplesCount; } private long getLargestQueuedTimestampUs() { long largestQueuedTimestampUs = Long.MIN_VALUE; - for (DefaultTrackOutput sampleQueue : sampleQueues) { + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, - sampleQueue.getLargestQueuedTimestampUs()); + sampleQueues.valueAt(i).getLargestQueuedTimestampUs()); } return largestQueuedTimestampUs; } @@ -523,7 +533,7 @@ import java.util.Arrays; @Override public void skipToKeyframeBefore(long timeUs) { - sampleQueues[track].skipToKeyframeBefore(timeUs); + sampleQueues.valueAt(track).skipToKeyframeBefore(timeUs); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java index e316215160..b9aa098b9d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -59,6 +59,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput // Accessed only on the loader thread. private boolean seenTrack; + private int seenTrackId; /** * @param extractor The extractor to wrap. @@ -116,8 +117,9 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput @Override public TrackOutput track(int id) { - Assertions.checkState(!seenTrack); + Assertions.checkState(!seenTrack || seenTrackId == id); seenTrack = true; + seenTrackId = id; return this; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java index b0ab90789c..3716c6d37f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java @@ -23,7 +23,6 @@ import java.io.File; import java.io.IOException; import java.io.PrintWriter; import junit.framework.Assert; -import junit.framework.TestCase; /** * A fake {@link ExtractorOutput}. @@ -37,8 +36,6 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab */ private static final boolean WRITE_DUMP = false; - private final boolean allowDuplicateTrackIds; - public final SparseArray trackOutputs; public int numberOfTracks; @@ -46,11 +43,6 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab public SeekMap seekMap; public FakeExtractorOutput() { - this(false); - } - - public FakeExtractorOutput(boolean allowDuplicateTrackIds) { - this.allowDuplicateTrackIds = allowDuplicateTrackIds; trackOutputs = new SparseArray<>(); } @@ -58,11 +50,10 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab public FakeTrackOutput track(int trackId) { FakeTrackOutput output = trackOutputs.get(trackId); if (output == null) { + Assert.assertFalse(tracksEnded); numberOfTracks++; output = new FakeTrackOutput(); trackOutputs.put(trackId, output); - } else { - TestCase.assertTrue("Duplicate track id: " + trackId, allowDuplicateTrackIds); } return output; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index 7b88062718..6f4578b694 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -267,8 +267,8 @@ public class TestUtil { */ public static FakeExtractorOutput assertOutput(Extractor extractor, String sampleFile, byte[] fileData, Instrumentation instrumentation, boolean simulateIOErrors, - boolean simulateUnknownLength, - boolean simulatePartialReads) throws IOException, InterruptedException { + boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException, + InterruptedException { FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData) .setSimulateIOErrors(simulateIOErrors) .setSimulateUnknownLength(simulateUnknownLength)