diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 150effebb1..9e0439dd12 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,25 @@ # Release notes # +### r2.0.3 ### + +This release contains important bug fixes. Users of r2.0.0, r2.0.1 and r2.0.2 +should proactively update to this version. + +* Fixed NullPointerException in ExtractorMediaSource + ([#1914](https://github.com/google/ExoPlayer/issues/1914). +* Fixed NullPointerException in HlsMediaPeriod + ([#1907](https://github.com/google/ExoPlayer/issues/1907). +* Fixed memory leak in PlaybackControlView + ([#1908](https://github.com/google/ExoPlayer/issues/1908). +* Fixed strict mode violation when using + SimpleExoPlayer.setVideoPlayerTextureView(). +* Fixed L3 Widevine provisioning + ([#1925](https://github.com/google/ExoPlayer/issues/1925). +* Fixed hiding of controls with use_controller="false" + ([#1919](https://github.com/google/ExoPlayer/issues/1919). +* Improvements to Cronet network stack extension. +* Misc bug fixes. + ### r2.0.2 ### * Fixes for MergingMediaSource and sideloaded subtitles. @@ -88,6 +108,13 @@ some of the motivations behind ExoPlayer 2.x * Suppressed "Sending message to a Handler on a dead thread" warnings ([#426](https://github.com/google/ExoPlayer/issues/426)). +### r1.5.12 ### + +* Improvements to Cronet network stack extension. +* Fix bug in demo app introduced in r1.5.11 that caused L3 Widevine + provisioning requests to fail. +* Misc bugfixes. + ### r1.5.11 ### * Cronet network stack extension. diff --git a/build.gradle b/build.gradle index 7a76fc92c3..c50dd31b27 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ allprojects { releaseRepoName = 'exoplayer' releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.0.2' + releaseVersion = 'r2.0.3' releaseWebsite = 'https://github.com/google/ExoPlayer' } } diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 1bba067c70..7fc0ac3d9c 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2003" + android:versionName="2.0.3"> diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index dcc5bc9b97..b0de0784de 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -46,7 +46,6 @@ import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceExcep import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Predicate; - import java.io.IOException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; @@ -82,7 +81,6 @@ public final class CronetDataSourceTest { private static final String TEST_CONTENT_TYPE = "test/test"; private static final byte[] TEST_POST_BODY = "test post body".getBytes(); private static final long TEST_CONTENT_LENGTH = 16000L; - private static final int TEST_BUFFER_SIZE = 16; private static final int TEST_CONNECTION_STATUS = 5; private DataSpec testDataSpec; @@ -231,10 +229,8 @@ public final class CronetDataSourceTest { @Test public void testRequestHeadersSet() throws HttpDataSourceException { - mockResponseStartSuccess(); - testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); - testResponseHeader.put("Content-Length", Long.toString(5000L)); + mockResponseStartSuccess(); dataSourceUnderTest.setRequestProperty("firstHeader", "firstValue"); dataSourceUnderTest.setRequestProperty("secondHeader", "secondValue"); @@ -257,13 +253,11 @@ public final class CronetDataSourceTest { @Test public void testRequestOpenGzippedCompressedReturnsDataSpecLength() throws HttpDataSourceException { + testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 5000, null); testResponseHeader.put("Content-Encoding", "gzip"); - testUrlResponseInfo = createUrlResponseInfo(200); // statusCode + testResponseHeader.put("Content-Length", Long.toString(50L)); mockResponseStartSuccess(); - // Data spec's requested length, 5000. Test response's length, 16,000. - testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); - assertEquals(5000 /* contentLength */, dataSourceUnderTest.open(testDataSpec)); verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec); } @@ -370,7 +364,7 @@ public final class CronetDataSourceTest { @Test public void testRequestReadTwice() throws HttpDataSourceException { mockResponseStartSuccess(); - mockReadSuccess(); + mockReadSuccess(0, 16); dataSourceUnderTest.open(testDataSpec); @@ -392,28 +386,23 @@ public final class CronetDataSourceTest { @Test public void testSecondRequestNoContentLength() throws HttpDataSourceException { mockResponseStartSuccess(); - mockReadSuccess(); - - byte[] returnedBuffer = new byte[8]; + testResponseHeader.put("Content-Length", Long.toString(1L)); + mockReadSuccess(0, 16); // First request. - testResponseHeader.put("Content-Length", Long.toString(1L)); - testUrlResponseInfo = createUrlResponseInfo(200); // statusCode dataSourceUnderTest.open(testDataSpec); + byte[] returnedBuffer = new byte[8]; dataSourceUnderTest.read(returnedBuffer, 0, 1); dataSourceUnderTest.close(); - // Second request. There's no Content-Length response header. testResponseHeader.remove("Content-Length"); - testUrlResponseInfo = createUrlResponseInfo(200); // statusCode + mockReadSuccess(0, 16); + + // Second request. dataSourceUnderTest.open(testDataSpec); returnedBuffer = new byte[16]; int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10); assertEquals(10, bytesRead); - - mockResponseFinished(); - - // Should read whats left in the buffer first. bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10); assertEquals(6, bytesRead); bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10); @@ -423,23 +412,54 @@ public final class CronetDataSourceTest { @Test public void testReadWithOffset() throws HttpDataSourceException { mockResponseStartSuccess(); - mockReadSuccess(); + mockReadSuccess(0, 16); dataSourceUnderTest.open(testDataSpec); byte[] returnedBuffer = new byte[16]; int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8); - assertArrayEquals(prefixZeros(buildTestDataArray(0, 8), 16), returnedBuffer); assertEquals(8, bytesRead); + assertArrayEquals(prefixZeros(buildTestDataArray(0, 8), 16), returnedBuffer); verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8); } + @Test + public void testRangeRequestWith206Response() throws HttpDataSourceException { + mockResponseStartSuccess(); + mockReadSuccess(1000, 5000); + testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests. + testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); + + dataSourceUnderTest.open(testDataSpec); + + byte[] returnedBuffer = new byte[16]; + int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16); + assertEquals(16, bytesRead); + assertArrayEquals(buildTestDataArray(1000, 16), returnedBuffer); + verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16); + } + + @Test + public void testRangeRequestWith200Response() throws HttpDataSourceException { + mockResponseStartSuccess(); + mockReadSuccess(0, 7000); + testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests. + testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); + + dataSourceUnderTest.open(testDataSpec); + + byte[] returnedBuffer = new byte[16]; + int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16); + assertEquals(16, bytesRead); + assertArrayEquals(buildTestDataArray(1000, 16), returnedBuffer); + verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16); + } + @Test public void testReadWithUnsetLength() throws HttpDataSourceException { testResponseHeader.remove("Content-Length"); - testUrlResponseInfo = createUrlResponseInfo(200); // statusCode mockResponseStartSuccess(); - mockReadSuccess(); + mockReadSuccess(0, 16); dataSourceUnderTest.open(testDataSpec); @@ -453,7 +473,7 @@ public final class CronetDataSourceTest { @Test public void testReadReturnsWhatItCan() throws HttpDataSourceException { mockResponseStartSuccess(); - mockReadSuccess(); + mockReadSuccess(0, 16); dataSourceUnderTest.open(testDataSpec); @@ -467,7 +487,7 @@ public final class CronetDataSourceTest { @Test public void testClosedMeansClosed() throws HttpDataSourceException { mockResponseStartSuccess(); - mockReadSuccess(); + mockReadSuccess(0, 16); int bytesRead = 0; dataSourceUnderTest.open(testDataSpec); @@ -493,32 +513,29 @@ public final class CronetDataSourceTest { @Test public void testOverread() throws HttpDataSourceException { - mockResponseStartSuccess(); - mockReadSuccess(); - - // Ask for 16 bytes - testDataSpec = new DataSpec(Uri.parse(TEST_URL), 10000, 16, null); - // Let the response promise to give 16 bytes back. + testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16, null); testResponseHeader.put("Content-Length", Long.toString(16L)); + mockResponseStartSuccess(); + mockReadSuccess(0, 16); dataSourceUnderTest.open(testDataSpec); byte[] returnedBuffer = new byte[8]; int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8); - assertArrayEquals(buildTestDataArray(0, 8), returnedBuffer); assertEquals(8, bytesRead); + assertArrayEquals(buildTestDataArray(0, 8), returnedBuffer); // The current buffer is kept if not completely consumed by DataSource reader. returnedBuffer = new byte[8]; bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 6); - assertArrayEquals(suffixZeros(buildTestDataArray(8, 6), 8), returnedBuffer); assertEquals(14, bytesRead); + assertArrayEquals(suffixZeros(buildTestDataArray(8, 6), 8), returnedBuffer); // 2 bytes left at this point. returnedBuffer = new byte[8]; bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8); - assertArrayEquals(suffixZeros(buildTestDataArray(14, 2), 8), returnedBuffer); assertEquals(16, bytesRead); + assertArrayEquals(suffixZeros(buildTestDataArray(14, 2), 8), returnedBuffer); // Should have only called read on cronet once. verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class)); @@ -752,16 +769,24 @@ public final class CronetDataSourceTest { }).when(mockUrlRequest).start(); } - private void mockReadSuccess() { + private void mockReadSuccess(int position, int length) { + final int[] positionAndRemaining = new int[] {position, length}; doAnswer(new Answer() { @Override public Void answer(InvocationOnMock invocation) throws Throwable { - ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0]; - inputBuffer.put(buildTestDataBuffer()); - dataSourceUnderTest.onReadCompleted( - mockUrlRequest, - testUrlResponseInfo, - inputBuffer); + if (positionAndRemaining[1] == 0) { + dataSourceUnderTest.onSucceeded(mockUrlRequest, testUrlResponseInfo); + } else { + ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0]; + int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining()); + inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength)); + positionAndRemaining[0] += readLength; + positionAndRemaining[1] -= readLength; + dataSourceUnderTest.onReadCompleted( + mockUrlRequest, + testUrlResponseInfo, + inputBuffer); + } return null; } }).when(mockUrlRequest).read(any(ByteBuffer.class)); @@ -780,16 +805,6 @@ public final class CronetDataSourceTest { }).when(mockUrlRequest).read(any(ByteBuffer.class)); } - private void mockResponseFinished() { - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.onSucceeded(mockUrlRequest, testUrlResponseInfo); - return null; - } - }).when(mockUrlRequest).read(any(ByteBuffer.class)); - } - private ConditionVariable buildUrlRequestStartedCondition() { final ConditionVariable startedCondition = new ConditionVariable(); doAnswer(new Answer() { @@ -802,8 +817,8 @@ public final class CronetDataSourceTest { return startedCondition; } - private static byte[] buildTestDataArray(int start, int length) { - return Arrays.copyOfRange(buildTestDataBuffer().array(), start, start + length); + private static byte[] buildTestDataArray(int position, int length) { + return buildTestDataBuffer(position, length).array(); } public static byte[] prefixZeros(byte[] data, int requiredLength) { @@ -816,10 +831,10 @@ public final class CronetDataSourceTest { return Arrays.copyOf(data, requiredLength); } - private static ByteBuffer buildTestDataBuffer() { - ByteBuffer testBuffer = ByteBuffer.allocate(TEST_BUFFER_SIZE); - for (byte i = 1; i <= TEST_BUFFER_SIZE; i++) { - testBuffer.put(i); + private static ByteBuffer buildTestDataBuffer(int position, int length) { + ByteBuffer testBuffer = ByteBuffer.allocate(length); + for (int i = 0; i < length; i++) { + testBuffer.put((byte) (position + i)); } testBuffer.flip(); return testBuffer; diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index a758f71f45..0190668a70 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -103,6 +103,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou // Accessed by the calling thread only. private boolean opened; + private long bytesToSkip; private long bytesRemaining; // Written from the calling thread only. currentUrlRequest.start() calls ensure writes are visible @@ -242,9 +243,10 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou } } - // TODO: Handle the case where we requested a range starting from a non-zero position and - // received a 200 rather than a 206. This occurs if the server does not support partial - // requests, and requires that the source skips to the requested position. + // If we requested a range starting from a non-zero position and received a 200 rather than a + // 206, then the server does not support partial requests. We'll need to manually skip to the + // requested position. + bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; // Calculate the content length. if (!getIsCompressed(responseInfo)) { @@ -281,7 +283,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou readBuffer = ByteBuffer.allocateDirect(READ_BUFFER_SIZE_BYTES); readBuffer.limit(0); } - if (!readBuffer.hasRemaining()) { + while (!readBuffer.hasRemaining()) { // Fill readBuffer with more data from Cronet. operation.close(); readBuffer.clear(); @@ -301,6 +303,12 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou } else { // The operation didn't time out, fail or finish, and therefore data must have been read. readBuffer.flip(); + Assertions.checkState(readBuffer.hasRemaining()); + if (bytesToSkip > 0) { + int bytesSkipped = (int) Math.min(readBuffer.remaining(), bytesToSkip); + readBuffer.position(readBuffer.position() + bytesSkipped); + bytesToSkip -= bytesSkipped; + } } } @@ -408,8 +416,10 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou executor, cronetEngine); // Set the headers. synchronized (requestProperties) { - if (dataSpec.postBody != null && !requestProperties.containsKey(CONTENT_TYPE)) { - throw new OpenException("POST request must set Content-Type", dataSpec, Status.IDLE); + if (dataSpec.postBody != null && dataSpec.postBody.length != 0 + && !requestProperties.containsKey(CONTENT_TYPE)) { + throw new OpenException("POST request with non-empty body must set Content-Type", dataSpec, + Status.IDLE); } for (Entry headerEntry : requestProperties.entrySet()) { requestBuilder.addHeader(headerEntry.getKey(), headerEntry.getValue()); @@ -426,10 +436,13 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou } requestBuilder.addHeader("Range", rangeValue.toString()); } - // Set the body. + // Set the method and (if non-empty) the body. if (dataSpec.postBody != null) { - requestBuilder.setUploadDataProvider(new ByteArrayUploadDataProvider(dataSpec.postBody), - executor); + requestBuilder.setHttpMethod("POST"); + if (dataSpec.postBody.length != 0) { + requestBuilder.setUploadDataProvider(new ByteArrayUploadDataProvider(dataSpec.postBody), + executor); + } } return requestBuilder.build(); } 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 7faea926e0..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 @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.extractor.ts; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator; +import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -66,9 +68,12 @@ public class AdtsReaderTest extends TestCase { @Override protected void setUp() throws Exception { - adtsOutput = new FakeTrackOutput(); - id3Output = new FakeTrackOutput(); - adtsReader = new AdtsReader(adtsOutput, id3Output); + FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); + adtsOutput = fakeExtractorOutput.track(0); + id3Output = fakeExtractorOutput.track(1); + adtsReader = new AdtsReader(true); + TrackIdGenerator idGenerator = new TrackIdGenerator(0, 1); + adtsReader.init(fakeExtractorOutput, idGenerator); data = new ParsableByteArray(TEST_DATA); firstFeed = true; } diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index dfbc9120c6..1f08507599 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.EsInfo; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput; @@ -72,7 +73,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { public void testCustomPesReader() throws Exception { CustomEsReaderFactory factory = new CustomEsReaderFactory(); - TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory); + TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false); FakeExtractorInput input = new FakeExtractorInput.Builder() .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts")) .setSimulateIOErrors(false) @@ -107,18 +108,25 @@ public final class TsExtractorTest extends InstrumentationTestCase { private static final class CustomEsReader extends ElementaryStreamReader { + private final String language; + private TrackOutput output; public int packetsRead = 0; - public CustomEsReader(TrackOutput output, String language) { - super(output); - output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0, - language, null, 0)); + public CustomEsReader(String language) { + this.language = language; } @Override public void seek() { } + @Override + public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + output = extractorOutput.track(idGenerator.getNextId()); + output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0, + language, null, 0)); + } + @Override public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { } @@ -148,16 +156,12 @@ public final class TsExtractorTest extends InstrumentationTestCase { } @Override - public ElementaryStreamReader onPmtEntry(int pid, int streamType, - ElementaryStreamReader.EsInfo esInfo, ExtractorOutput output) { + public ElementaryStreamReader createStreamReader(int streamType, EsInfo esInfo) { if (streamType == 3) { - // We need to manually avoid a duplicate custom reader creation. - if (reader == null) { - reader = new CustomEsReader(output.track(pid), esInfo.language); - } + reader = new CustomEsReader(esInfo.language); return reader; } else { - return defaultFactory.onPmtEntry(pid, streamType, esInfo, output); + return defaultFactory.createStreamReader(streamType, esInfo); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 9e212ba2f6..23e6d4d593 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo { /** * The version of the library, expressed as a string. */ - String VERSION = "2.0.2"; + String VERSION = "2.0.3"; /** * The version of the library, expressed as an integer. @@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo { * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding * integer version 123045006 (123-045-006). */ - int VERSION_INT = 2000002; + int VERSION_INT = 2000003; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 5d467d906d..f41706091d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -108,6 +108,7 @@ public final class SimpleExoPlayer implements ExoPlayer { private Format audioFormat; private Surface surface; + private boolean ownsSurface; private SurfaceHolder surfaceHolder; private TextureView textureView; private TextRenderer.Output textOutput; @@ -206,7 +207,7 @@ public final class SimpleExoPlayer implements ExoPlayer { */ public void setVideoSurface(Surface surface) { removeSurfaceCallbacks(); - setVideoSurfaceInternal(surface); + setVideoSurfaceInternal(surface, false); } /** @@ -219,9 +220,9 @@ public final class SimpleExoPlayer implements ExoPlayer { removeSurfaceCallbacks(); this.surfaceHolder = surfaceHolder; if (surfaceHolder == null) { - setVideoSurfaceInternal(null); + setVideoSurfaceInternal(null, false); } else { - setVideoSurfaceInternal(surfaceHolder.getSurface()); + setVideoSurfaceInternal(surfaceHolder.getSurface(), false); surfaceHolder.addCallback(componentListener); } } @@ -246,13 +247,13 @@ public final class SimpleExoPlayer implements ExoPlayer { removeSurfaceCallbacks(); this.textureView = textureView; if (textureView == null) { - setVideoSurfaceInternal(null); + setVideoSurfaceInternal(null, true); } else { if (textureView.getSurfaceTextureListener() != null) { Log.w(TAG, "Replacing existing SurfaceTextureListener."); } SurfaceTexture surfaceTexture = textureView.getSurfaceTexture(); - setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture)); + setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true); textureView.setSurfaceTextureListener(componentListener); } } @@ -477,6 +478,12 @@ public final class SimpleExoPlayer implements ExoPlayer { public void release() { player.release(); removeSurfaceCallbacks(); + if (surface != null) { + if (ownsSurface) { + surface.release(); + } + surface = null; + } } @Override @@ -627,8 +634,9 @@ public final class SimpleExoPlayer implements ExoPlayer { } } - private void setVideoSurfaceInternal(Surface surface) { - this.surface = surface; + private void setVideoSurfaceInternal(Surface surface, boolean ownsSurface) { + // Note: We don't turn this method into a no-op if the surface is being replaced with itself + // so as to ensure onRenderedFirstFrame callbacks are still called in this case. ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; int count = 0; for (Renderer renderer : renderers) { @@ -636,12 +644,18 @@ public final class SimpleExoPlayer implements ExoPlayer { messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SURFACE, surface); } } - if (surface == null) { - // Block to ensure that the surface is not accessed after the method returns. + if (this.surface != null && this.surface != surface) { + // If we created this surface, we are responsible for releasing it. + if (this.ownsSurface) { + this.surface.release(); + } + // We're replacing a surface. Block to ensure that it's not accessed after the method returns. player.blockingSendMessages(messages); } else { player.sendMessages(messages); } + this.surface = surface; + this.ownsSurface = ownsSurface; } private final class ComponentListener implements VideoRendererEventListener, @@ -790,7 +804,7 @@ public final class SimpleExoPlayer implements ExoPlayer { @Override public void surfaceCreated(SurfaceHolder holder) { - setVideoSurfaceInternal(holder.getSurface()); + setVideoSurfaceInternal(holder.getSurface(), false); } @Override @@ -800,14 +814,14 @@ public final class SimpleExoPlayer implements ExoPlayer { @Override public void surfaceDestroyed(SurfaceHolder holder) { - setVideoSurfaceInternal(null); + setVideoSurfaceInternal(null, false); } // TextureView.SurfaceTextureListener implementation @Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { - setVideoSurfaceInternal(new Surface(surfaceTexture)); + setVideoSurfaceInternal(new Surface(surfaceTexture), true); } @Override @@ -817,7 +831,7 @@ public final class SimpleExoPlayer implements ExoPlayer { @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { - setVideoSurface(null); + setVideoSurfaceInternal(null, true); return true; } diff --git a/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index 14329d47ee..65e41dd91e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -71,7 +71,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { @Override public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); - return executePost(url, null, null); + return executePost(url, new byte[0], null); } @Override @@ -81,6 +81,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { url = defaultUrl; } Map requestProperties = new HashMap<>(); + requestProperties.put("Content-Type", "application/octet-stream"); if (C.PLAYREADY_UUID.equals(uuid)) { requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES); } @@ -93,8 +94,6 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { private byte[] executePost(String url, byte[] data, Map requestProperties) throws IOException { HttpDataSource dataSource = dataSourceFactory.createDataSource(); - // Note: This will be overridden by a Content-Type in requestProperties, if one is set. - dataSource.setRequestProperty("Content-Type", "application/octet-stream"); if (requestProperties != null) { for (Map.Entry requestProperty : requestProperties.entrySet()) { dataSource.setRequestProperty(requestProperty.getKey(), requestProperty.getValue()); 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/Ac3Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java index 979c7244a8..7fc8b429a8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -117,7 +118,8 @@ public final class Ac3Extractor implements Extractor { @Override public void init(ExtractorOutput output) { - reader = new Ac3Reader(output.track(0)); // TODO: Add support for embedded ID3. + reader = new Ac3Reader(); // TODO: Add support for embedded ID3. + reader.init(output, new TrackIdGenerator(0, 1)); output.endTracks(); output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java index cbe6d2e9c8..a9d3319f87 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.Ac3Util; +import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -37,6 +38,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private final ParsableByteArray headerScratchBytes; private final String language; + private TrackOutput output; + private int state; private int bytesRead; @@ -54,21 +57,17 @@ import com.google.android.exoplayer2.util.ParsableByteArray; /** * Constructs a new reader for (E-)AC-3 elementary streams. - * - * @param output Track output for extracted samples. */ - public Ac3Reader(TrackOutput output) { - this(output, null); + public Ac3Reader() { + this(null); } /** * Constructs a new reader for (E-)AC-3 elementary streams. * - * @param output Track output for extracted samples. * @param language Track language. */ - public Ac3Reader(TrackOutput output, String language) { - super(output); + public Ac3Reader(String language) { headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]); headerScratchBytes = new ParsableByteArray(headerScratchBits.data); state = STATE_FINDING_SYNC; @@ -82,6 +81,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray; lastByteWas0B = false; } + @Override + public void init(ExtractorOutput extractorOutput, TrackIdGenerator generator) { + output = extractorOutput.track(generator.getNextId()); + } + @Override public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { timeUs = pesTimeUs; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index f131d8997b..7a9cbd4bb1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -126,7 +127,8 @@ public final class AdtsExtractor implements Extractor { @Override public void init(ExtractorOutput output) { - reader = new AdtsReader(output.track(0), output.track(1)); + reader = new AdtsReader(true); + reader.init(output, new TrackIdGenerator(0, 1)); output.endTracks(); output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index ac493c7d32..d0474f7e44 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -19,6 +19,8 @@ import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.DummyTrackOutput; +import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; @@ -53,11 +55,14 @@ import java.util.Collections; private static final int ID3_SIZE_OFFSET = 6; private static final byte[] ID3_IDENTIFIER = {'I', 'D', '3'}; + private final boolean exposeId3; private final ParsableBitArray adtsScratch; private final ParsableByteArray id3HeaderBuffer; - private final TrackOutput id3Output; private final String language; + private TrackOutput output; + private TrackOutput id3Output; + private int state; private int bytesRead; @@ -77,26 +82,21 @@ import java.util.Collections; private long currentSampleDuration; /** - * @param output A {@link TrackOutput} to which AAC samples should be written. - * @param id3Output A {@link TrackOutput} to which ID3 samples should be written. + * @param exposeId3 True if the reader should expose ID3 information. */ - public AdtsReader(TrackOutput output, TrackOutput id3Output) { - this(output, id3Output, null); + public AdtsReader(boolean exposeId3) { + this(exposeId3, null); } /** - * @param output A {@link TrackOutput} to which AAC samples should be written. - * @param id3Output A {@link TrackOutput} to which ID3 samples should be written. + * @param exposeId3 True if the reader should expose ID3 information. * @param language Track language. */ - public AdtsReader(TrackOutput output, TrackOutput id3Output, String language) { - super(output); - this.id3Output = id3Output; - id3Output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, - Format.NO_VALUE, null)); + public AdtsReader(boolean exposeId3, String language) { adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE)); setFindingSampleState(); + this.exposeId3 = exposeId3; this.language = language; } @@ -105,6 +105,18 @@ import java.util.Collections; setFindingSampleState(); } + @Override + public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + output = extractorOutput.track(idGenerator.getNextId()); + if (exposeId3) { + id3Output = extractorOutput.track(idGenerator.getNextId()); + id3Output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, + Format.NO_VALUE, null)); + } else { + id3Output = new DummyTrackOutput(); + } + } + @Override public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { timeUs = pesTimeUs; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultStreamReaderFactory.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultStreamReaderFactory.java index d5e3b78cfd..58a0e55f02 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultStreamReaderFactory.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultStreamReaderFactory.java @@ -16,9 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.support.annotation.IntDef; -import android.util.SparseBooleanArray; -import com.google.android.exoplayer2.extractor.DummyTrackOutput; -import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.EsInfo; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -28,80 +26,54 @@ import java.lang.annotation.RetentionPolicy; public final class DefaultStreamReaderFactory implements ElementaryStreamReader.Factory { /** - * Flags controlling what workarounds are enabled for elementary stream readers. + * Flags controlling elementary stream readers behaviour. */ @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, value = {WORKAROUND_ALLOW_NON_IDR_KEYFRAMES, WORKAROUND_IGNORE_AAC_STREAM, - WORKAROUND_IGNORE_H264_STREAM, WORKAROUND_DETECT_ACCESS_UNITS, WORKAROUND_MAP_BY_TYPE}) - public @interface WorkaroundFlags { + @IntDef(flag = true, value = {FLAG_ALLOW_NON_IDR_KEYFRAMES, FLAG_IGNORE_AAC_STREAM, + FLAG_IGNORE_H264_STREAM, FLAG_DETECT_ACCESS_UNITS}) + public @interface Flags { } - public static final int WORKAROUND_ALLOW_NON_IDR_KEYFRAMES = 1; - public static final int WORKAROUND_IGNORE_AAC_STREAM = 2; - public static final int WORKAROUND_IGNORE_H264_STREAM = 4; - public static final int WORKAROUND_DETECT_ACCESS_UNITS = 8; - public static final int WORKAROUND_MAP_BY_TYPE = 16; + public static final int FLAG_ALLOW_NON_IDR_KEYFRAMES = 1; + public static final int FLAG_IGNORE_AAC_STREAM = 2; + public static final int FLAG_IGNORE_H264_STREAM = 4; + public static final int FLAG_DETECT_ACCESS_UNITS = 8; - private static final int BASE_EMBEDDED_TRACK_ID = 0x2000; // 0xFF + 1. - - private final SparseBooleanArray trackIds; - @WorkaroundFlags - private final int workaroundFlags; - private Id3Reader id3Reader; - private int nextEmbeddedTrackId = BASE_EMBEDDED_TRACK_ID; + @Flags + private final int flags; public DefaultStreamReaderFactory() { this(0); } - public DefaultStreamReaderFactory(int workaroundFlags) { - trackIds = new SparseBooleanArray(); - this.workaroundFlags = workaroundFlags; + public DefaultStreamReaderFactory(@Flags int flags) { + this.flags = flags; } @Override - public ElementaryStreamReader onPmtEntry(int pid, int streamType, - ElementaryStreamReader.EsInfo esInfo, ExtractorOutput output) { - - if ((workaroundFlags & WORKAROUND_MAP_BY_TYPE) != 0 && id3Reader == null) { - // Setup an ID3 track regardless of whether there's a corresponding entry, in case one - // appears intermittently during playback. See b/20261500. - id3Reader = new Id3Reader(output.track(TsExtractor.TS_STREAM_TYPE_ID3)); - } - int trackId = (workaroundFlags & WORKAROUND_MAP_BY_TYPE) != 0 ? streamType : pid; - if (trackIds.get(trackId)) { - return null; - } - trackIds.put(trackId, true); + public ElementaryStreamReader createStreamReader(int streamType, EsInfo esInfo) { switch (streamType) { case TsExtractor.TS_STREAM_TYPE_MPA: case TsExtractor.TS_STREAM_TYPE_MPA_LSF: - return new MpegAudioReader(output.track(trackId), esInfo.language); + return new MpegAudioReader(esInfo.language); case TsExtractor.TS_STREAM_TYPE_AAC: - return (workaroundFlags & WORKAROUND_IGNORE_AAC_STREAM) != 0 ? null - : new AdtsReader(output.track(trackId), new DummyTrackOutput(), esInfo.language); + return (flags & FLAG_IGNORE_AAC_STREAM) != 0 ? null + : new AdtsReader(false, esInfo.language); case TsExtractor.TS_STREAM_TYPE_AC3: case TsExtractor.TS_STREAM_TYPE_E_AC3: - return new Ac3Reader(output.track(trackId), esInfo.language); + return new Ac3Reader(esInfo.language); case TsExtractor.TS_STREAM_TYPE_DTS: case TsExtractor.TS_STREAM_TYPE_HDMV_DTS: - return new DtsReader(output.track(trackId), esInfo.language); + return new DtsReader(esInfo.language); case TsExtractor.TS_STREAM_TYPE_H262: - return new H262Reader(output.track(trackId)); + return new H262Reader(); case TsExtractor.TS_STREAM_TYPE_H264: - return (workaroundFlags & WORKAROUND_IGNORE_H264_STREAM) != 0 - ? null : new H264Reader(output.track(trackId), - new SeiReader(output.track(nextEmbeddedTrackId++)), - (workaroundFlags & WORKAROUND_ALLOW_NON_IDR_KEYFRAMES) != 0, - (workaroundFlags & WORKAROUND_DETECT_ACCESS_UNITS) != 0); + return (flags & FLAG_IGNORE_H264_STREAM) != 0 ? null + : new H264Reader((flags & FLAG_ALLOW_NON_IDR_KEYFRAMES) != 0, + (flags & FLAG_DETECT_ACCESS_UNITS) != 0); case TsExtractor.TS_STREAM_TYPE_H265: - return new H265Reader(output.track(trackId), - new SeiReader(output.track(nextEmbeddedTrackId++))); + return new H265Reader(); case TsExtractor.TS_STREAM_TYPE_ID3: - if ((workaroundFlags & WORKAROUND_MAP_BY_TYPE) != 0) { - return id3Reader; - } else { - return new Id3Reader(output.track(nextEmbeddedTrackId++)); - } + return new Id3Reader(); default: return null; } diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java index e2112df755..42223ef285 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.DtsUtil; +import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -37,6 +38,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private final ParsableByteArray headerScratchBytes; private final String language; + private TrackOutput output; + private int state; private int bytesRead; @@ -54,20 +57,9 @@ import com.google.android.exoplayer2.util.ParsableByteArray; /** * Constructs a new reader for DTS elementary streams. * - * @param output Track output for extracted samples. - */ - public DtsReader(TrackOutput output) { - this(output, null); - } - - /** - * Constructs a new reader for DTS elementary streams. - * - * @param output Track output for extracted samples. * @param language Track language. */ - public DtsReader(TrackOutput output, String language) { - super(output); + public DtsReader(String language) { headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]); headerScratchBytes.data[0] = (byte) ((SYNC_VALUE >> 24) & 0xFF); headerScratchBytes.data[1] = (byte) ((SYNC_VALUE >> 16) & 0xFF); @@ -84,6 +76,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray; syncBytes = 0; } + @Override + public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + output = extractorOutput.track(idGenerator.getNextId()); + } + @Override public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { timeUs = pesTimeUs; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java index 7a220c98b3..e2efbebb43 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java @@ -33,17 +33,12 @@ public abstract class ElementaryStreamReader { * Returns an {@link ElementaryStreamReader} for a given PMT entry. May return null if the * stream type is not supported or if the stream already has a reader assigned to it. * - * @param pid The pid for the PMT entry. - * @param streamType One of the {@link TsExtractor}{@code .TS_STREAM_TYPE_*} constants defining - * the type of the stream. - * @param esInfo The descriptor information linked to the elementary stream. - * @param output The {@link ExtractorOutput} that provides the {@link TrackOutput}s for the - * created readers. + * @param streamType Stream type value as defined in the PMT entry or associated descriptors. + * @param esInfo Information associated to the elementary stream provided in the PMT. * @return An {@link ElementaryStreamReader} for the elementary streams carried by the provided * pid. {@code null} if the stream is not supported or if it should be ignored. */ - ElementaryStreamReader onPmtEntry(int pid, int streamType, EsInfo esInfo, - ExtractorOutput output); + ElementaryStreamReader createStreamReader(int streamType, EsInfo esInfo); } @@ -70,13 +65,24 @@ public abstract class ElementaryStreamReader { } - protected final TrackOutput output; - /** - * @param output A {@link TrackOutput} to which samples should be written. + * Generates track ids for initializing {@link ElementaryStreamReader}s' {@link TrackOutput}s. */ - protected ElementaryStreamReader(TrackOutput output) { - this.output = output; + public static final class TrackIdGenerator { + + private final int firstId; + private final int idIncrement; + private int generatedIdCount; + + public TrackIdGenerator(int firstId, int idIncrement) { + this.firstId = firstId; + this.idIncrement = idIncrement; + } + + public int getNextId() { + return firstId + idIncrement * generatedIdCount++; + } + } /** @@ -84,6 +90,15 @@ public abstract class ElementaryStreamReader { */ public abstract void seek(); + /** + * Initializes the reader by providing outputs and ids for the tracks. + * + * @param extractorOutput The {@link ExtractorOutput} that receives the extracted data. + * @param idGenerator A {@link TrackIdGenerator} that generates unique track ids for the + * {@link TrackOutput}s. + */ + public abstract void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator); + /** * Called when a packet starts. * diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index cdbd8e391d..fbfe7e1209 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; @@ -35,6 +36,8 @@ import java.util.Collections; private static final int START_EXTENSION = 0xB5; private static final int START_GROUP = 0xB8; + private TrackOutput output; + // Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4. private static final double[] FRAME_RATE_VALUES = new double[] { 24000d / 1001, 24, 25, 30000d / 1001, 30, 50, 60000d / 1001, 60}; @@ -58,8 +61,7 @@ import java.util.Collections; private long framePosition; private long frameTimeUs; - public H262Reader(TrackOutput output) { - super(output); + public H262Reader() { prefixFlags = new boolean[4]; csdBuffer = new CsdBuffer(128); } @@ -73,6 +75,11 @@ import java.util.Collections; totalBytesWritten = 0; } + @Override + public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + output = extractorOutput.track(idGenerator.getNextId()); + } + @Override public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { pesPtsUsAvailable = pesTimeUs != C.TIME_UNSET; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java index ce7b7e6383..6fee9ea6d7 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H264Reader.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; @@ -37,17 +38,20 @@ import java.util.List; private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set - // State that should not be reset on seek. - private boolean hasOutputFormat; - - // State that should be reset on seek. - private final SeiReader seiReader; - private final boolean[] prefixFlags; - private final SampleReader sampleReader; + private final boolean allowNonIdrKeyframes; + private final boolean detectAccessUnits; private final NalUnitTargetBuffer sps; private final NalUnitTargetBuffer pps; private final NalUnitTargetBuffer sei; private long totalBytesWritten; + private final boolean[] prefixFlags; + + private TrackOutput output; + private SeiReader seiReader; + private SampleReader sampleReader; + + // State that should not be reset on seek. + private boolean hasOutputFormat; // Per packet state that gets reset at the start of each packet. private long pesTimeUs; @@ -56,19 +60,15 @@ import java.util.List; private final ParsableByteArray seiWrapper; /** - * @param output A {@link TrackOutput} to which H.264 samples should be written. - * @param seiReader A reader for CEA-608 samples in SEI NAL units. * @param allowNonIdrKeyframes Whether to treat samples consisting of non-IDR I slices as * synchronization samples (key-frames). * @param detectAccessUnits Whether to split the input stream into access units (samples) based on * slice headers. Pass {@code false} if the stream contains access unit delimiters (AUDs). */ - public H264Reader(TrackOutput output, SeiReader seiReader, boolean allowNonIdrKeyframes, - boolean detectAccessUnits) { - super(output); - this.seiReader = seiReader; + public H264Reader(boolean allowNonIdrKeyframes, boolean detectAccessUnits) { prefixFlags = new boolean[3]; - sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits); + this.allowNonIdrKeyframes = allowNonIdrKeyframes; + this.detectAccessUnits = detectAccessUnits; sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128); pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); @@ -85,6 +85,13 @@ import java.util.List; totalBytesWritten = 0; } + @Override + public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + output = extractorOutput.track(idGenerator.getNextId()); + sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits); + seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId())); + } + @Override public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { this.pesTimeUs = pesTimeUs; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index c8828cefa6..6283371a19 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; @@ -42,11 +43,13 @@ import java.util.Collections; private static final int PREFIX_SEI_NUT = 39; private static final int SUFFIX_SEI_NUT = 40; + private TrackOutput output; + private SeiReader seiReader; + // State that should not be reset on seek. private boolean hasOutputFormat; // State that should be reset on seek. - private final SeiReader seiReader; private final boolean[] prefixFlags; private final NalUnitTargetBuffer vps; private final NalUnitTargetBuffer sps; @@ -62,13 +65,7 @@ import java.util.Collections; // Scratch variables to avoid allocations. private final ParsableByteArray seiWrapper; - /** - * @param output A {@link TrackOutput} to which H.265 samples should be written. - * @param seiReader A reader for CEA-608 samples in SEI NAL units. - */ - public H265Reader(TrackOutput output, SeiReader seiReader) { - super(output); - this.seiReader = seiReader; + public H265Reader() { prefixFlags = new boolean[3]; vps = new NalUnitTargetBuffer(VPS_NUT, 128); sps = new NalUnitTargetBuffer(SPS_NUT, 128); @@ -91,6 +88,12 @@ import java.util.Collections; totalBytesWritten = 0; } + @Override + public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + output = extractorOutput.track(idGenerator.getNextId()); + seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId())); + } + @Override public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { this.pesTimeUs = pesTimeUs; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index 1001f1a1ae..2c657d4aca 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -30,6 +31,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private final ParsableByteArray id3Header; + private TrackOutput output; + // State that should be reset on seek. private boolean writingSample; @@ -38,10 +41,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private int sampleSize; private int sampleBytesRead; - public Id3Reader(TrackOutput output) { - super(output); - output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, - null)); + public Id3Reader() { id3Header = new ParsableByteArray(ID3_HEADER_SIZE); } @@ -50,6 +50,13 @@ import com.google.android.exoplayer2.util.ParsableByteArray; writingSample = false; } + @Override + public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + output = extractorOutput.track(idGenerator.getNextId()); + output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, + null)); + } + @Override public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { if (!dataAlignmentIndicator) { diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java index c78882c2c9..d25d0703ae 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -36,6 +37,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; private final MpegAudioHeader header; private final String language; + private TrackOutput output; + private int state; private int frameBytesRead; private boolean hasOutputFormat; @@ -50,12 +53,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray; // The timestamp to attach to the next sample in the current packet. private long timeUs; - public MpegAudioReader(TrackOutput output) { - this(output, null); + public MpegAudioReader() { + this(null); } - public MpegAudioReader(TrackOutput output, String language) { - super(output); + public MpegAudioReader(String language) { state = STATE_FINDING_HEADER; // The first byte of an MPEG Audio frame header is always 0xFF. headerScratch = new ParsableByteArray(4); @@ -71,6 +73,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray; lastByteWasFF = false; } + @Override + public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + output = extractorOutput.track(idGenerator.getNextId()); + } + @Override public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { timeUs = pesTimeUs; diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java index 35eb519f09..b615a3e8ee 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TimestampAdjuster; +import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; @@ -49,6 +50,7 @@ public final class PsExtractor implements Extractor { private static final int SYSTEM_HEADER_START_CODE = 0x000001BB; private static final int PACKET_START_CODE_PREFIX = 0x000001; private static final int MPEG_PROGRAM_END_CODE = 0x000001B9; + private static final int MAX_STREAM_ID_PLUS_ONE = 0x100; private static final long MAX_SEARCH_LENGTH = 1024 * 1024; public static final int PRIVATE_STREAM_1 = 0xBD; @@ -189,16 +191,18 @@ public final class PsExtractor implements Extractor { // Private stream, used for AC3 audio. // NOTE: This may need further parsing to determine if its DTS, but that's likely only // valid for DVDs. - elementaryStreamReader = new Ac3Reader(output.track(streamId)); + elementaryStreamReader = new Ac3Reader(); foundAudioTrack = true; } else if (!foundAudioTrack && (streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) { - elementaryStreamReader = new MpegAudioReader(output.track(streamId)); + elementaryStreamReader = new MpegAudioReader(); foundAudioTrack = true; } else if (!foundVideoTrack && (streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) { - elementaryStreamReader = new H262Reader(output.track(streamId)); + elementaryStreamReader = new H262Reader(); foundVideoTrack = true; } if (elementaryStreamReader != null) { + TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE); + elementaryStreamReader.init(output, idGenerator); payloadReader = new PesReader(elementaryStreamReader, timestampAdjuster); psPayloadReaders.put(streamId, payloadReader); } 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 0248db9650..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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.Log; import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; @@ -26,6 +27,9 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TimestampAdjuster; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.EsInfo; +import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -50,12 +54,6 @@ public final class TsExtractor implements Extractor { }; - private static final String TAG = "TsExtractor"; - - private static final int TS_PACKET_SIZE = 188; - private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. - private static final int TS_PAT_PID = 0; - 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; @@ -68,6 +66,12 @@ public final class TsExtractor implements Extractor { public static final int TS_STREAM_TYPE_H265 = 0x24; public static final int TS_STREAM_TYPE_ID3 = 0x15; + private static final String TAG = "TsExtractor"; + + private static final int TS_PACKET_SIZE = 188; + private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. + private static final int TS_PAT_PID = 0; + private static final int MAX_PID_PLUS_ONE = 0x2000; private static final long AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("AC-3"); private static final long E_AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("EAC3"); @@ -76,15 +80,19 @@ public final class TsExtractor implements Extractor { private static final int BUFFER_PACKET_COUNT = 5; // Should be at least 2 private static final int BUFFER_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT; + private final boolean mapByType; private final TimestampAdjuster timestampAdjuster; private final ParsableByteArray tsPacketBuffer; private final ParsableBitArray tsScratch; private final SparseIntArray continuityCounters; private final ElementaryStreamReader.Factory streamReaderFactory; - /* package */ final SparseArray tsPayloadReaders; // Indexed by pid + private final SparseArray tsPayloadReaders; // Indexed by pid + private final SparseBooleanArray trackIds; // Accessed only by the loading thread. private ExtractorOutput output; + private boolean tracksEnded; + private ElementaryStreamReader id3Reader; public TsExtractor() { this(new TimestampAdjuster(0)); @@ -94,22 +102,26 @@ public final class TsExtractor implements Extractor { * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. */ public TsExtractor(TimestampAdjuster timestampAdjuster) { - this(timestampAdjuster, new DefaultStreamReaderFactory()); + this(timestampAdjuster, new DefaultStreamReaderFactory(), false); } /** * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. * @param customReaderFactory Factory for injecting a custom set of elementary stream readers. + * @param mapByType True if {@link TrackOutput}s should be mapped by their type, false to map them + * by their PID. */ public TsExtractor(TimestampAdjuster timestampAdjuster, - ElementaryStreamReader.Factory customReaderFactory) { + ElementaryStreamReader.Factory customReaderFactory, boolean mapByType) { this.timestampAdjuster = timestampAdjuster; this.streamReaderFactory = Assertions.checkNotNull(customReaderFactory); + this.mapByType = mapByType; tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE); 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. @@ -141,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 @@ -240,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. */ @@ -333,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)); } } } @@ -353,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 @@ -413,6 +433,14 @@ public final class TsExtractor implements Extractor { // Skip the descriptors. sectionData.skipBytes(programInfoLength); + if (mapByType && id3Reader == null) { + // Setup an ID3 track regardless of whether there's a corresponding entry, in case one + // appears intermittently during playback. See [Internal: b/20261500]. + EsInfo dummyEsInfo = new EsInfo(TS_STREAM_TYPE_ID3, null, new byte[0]); + id3Reader = streamReaderFactory.createStreamReader(TS_STREAM_TYPE_ID3, dummyEsInfo); + id3Reader.init(output, new TrackIdGenerator(TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); + } + int remainingEntriesLength = sectionLength - 9 /* Length of fields before descriptors */ - programInfoLength - 4 /* CRC length */; while (remainingEntriesLength > 0) { @@ -422,21 +450,40 @@ public final class TsExtractor implements Extractor { int elementaryPid = pmtScratch.readBits(13); pmtScratch.skipBits(4); // reserved int esInfoLength = pmtScratch.readBits(12); // ES_info_length. - ElementaryStreamReader.EsInfo esInfo = readEsInfo(sectionData, esInfoLength); + EsInfo esInfo = readEsInfo(sectionData, esInfoLength); if (streamType == 0x06) { streamType = esInfo.streamType; } remainingEntriesLength -= esInfoLength + 5; - ElementaryStreamReader pesPayloadReader = streamReaderFactory.onPmtEntry(elementaryPid, - streamType, esInfo, output); + + int trackId = mapByType ? streamType : elementaryPid; + if (trackIds.get(trackId)) { + continue; + } + trackIds.put(trackId, true); + + ElementaryStreamReader pesPayloadReader; + if (mapByType && streamType == TS_STREAM_TYPE_ID3) { + pesPayloadReader = id3Reader; + } else { + pesPayloadReader = streamReaderFactory.createStreamReader(streamType, esInfo); + pesPayloadReader.init(output, new TrackIdGenerator(trackId, MAX_PID_PLUS_ONE)); + } if (pesPayloadReader != null) { - tsPayloadReaders.put(elementaryPid, - new PesReader(pesPayloadReader, timestampAdjuster)); + 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; } /** @@ -447,7 +494,7 @@ public final class TsExtractor implements Extractor { * @param length The length of descriptors to read from the current position in {@code data}. * @return The stream info read from the available descriptors. */ - private ElementaryStreamReader.EsInfo readEsInfo(ParsableByteArray data, int length) { + private EsInfo readEsInfo(ParsableByteArray data, int length) { int descriptorsStartPosition = data.getPosition(); int descriptorsEndPosition = descriptorsStartPosition + length; int streamType = -1; @@ -479,7 +526,7 @@ public final class TsExtractor implements Extractor { data.skipBytes(positionOfNextDescriptor - data.getPosition()); } data.setPosition(descriptorsEndPosition); - return new ElementaryStreamReader.EsInfo(streamType, language, + return new EsInfo(streamType, language, Arrays.copyOfRange(sectionData.data, descriptorsStartPosition, descriptorsEndPosition)); } 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/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 78cb8f5c7f..b2f0ae6f98 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -655,7 +655,9 @@ public class DashManifestParser extends DefaultHandler return MimeTypes.getVideoMediaMimeType(codecs); } else if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { if (codecs != null) { - if (codecs.contains("eia608") || codecs.contains("cea608")) { + if (codecs.contains("cea708")) { + return MimeTypes.APPLICATION_CEA708; + } else if (codecs.contains("eia608") || codecs.contains("cea608")) { return MimeTypes.APPLICATION_CEA608; } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index caca5d9b83..53d9e70d76 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -257,8 +257,16 @@ import java.util.Locale; chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex, oldVariantIndex, newVariantIndex); if (chunkMediaSequence < mediaPlaylist.mediaSequence) { - fatalError = new BehindLiveWindowException(); - return; + // We try getting the next chunk without adapting in case that's the reason for falling + // behind the live window. + newVariantIndex = oldVariantIndex; + mediaPlaylist = variantPlaylists[newVariantIndex]; + chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex, oldVariantIndex, + newVariantIndex); + if (chunkMediaSequence < mediaPlaylist.mediaSequence) { + fatalError = new BehindLiveWindowException(); + return; + } } } } else { @@ -369,29 +377,29 @@ import java.util.Locale; } } else if (needNewExtractor) { // MPEG-2 TS segments, but we need a new extractor. - // This flag ensures the change of pid between streams does not affect the sample queues. - @DefaultStreamReaderFactory.WorkaroundFlags - int workaroundFlags = DefaultStreamReaderFactory.WORKAROUND_MAP_BY_TYPE; - String codecs = variants[newVariantIndex].format.codecs; - if (!TextUtils.isEmpty(codecs)) { - // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really - // exist. If we know from the codec attribute that they don't exist, then we can explicitly - // ignore them even if they're declared. - if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) { - workaroundFlags |= DefaultStreamReaderFactory.WORKAROUND_IGNORE_AAC_STREAM; - } - if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) { - workaroundFlags |= DefaultStreamReaderFactory.WORKAROUND_IGNORE_H264_STREAM; - } - } isTimestampMaster = true; if (useInitializedExtractor) { extractor = lastLoadedInitializationChunk.extractor; } else { timestampAdjuster = timestampAdjusterProvider.getAdjuster( segment.discontinuitySequenceNumber, startTimeUs); + // This flag ensures the change of pid between streams does not affect the sample queues. + @DefaultStreamReaderFactory.Flags + int esReaderFactoryFlags = 0; + String codecs = variants[newVariantIndex].format.codecs; + if (!TextUtils.isEmpty(codecs)) { + // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really + // exist. If we know from the codec attribute that they don't exist, then we can + // explicitly ignore them even if they're declared. + if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) { + esReaderFactoryFlags |= DefaultStreamReaderFactory.FLAG_IGNORE_AAC_STREAM; + } + if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) { + esReaderFactoryFlags |= DefaultStreamReaderFactory.FLAG_IGNORE_H264_STREAM; + } + } extractor = new TsExtractor(timestampAdjuster, - new DefaultStreamReaderFactory(workaroundFlags)); + new DefaultStreamReaderFactory(esReaderFactoryFlags), true); } } else { // MPEG-2 TS segments, and we need to continue using the same extractor. 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 57925ed67a..f4c8177f21 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 @@ -328,7 +328,7 @@ import java.util.List; sampleStreamWrappers = new HlsSampleStreamWrapper[] { buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, null, null)}; pendingPrepareCount = 1; - sampleStreamWrappers[0].prepare(); + sampleStreamWrappers[0].continuePreparing(); return; } @@ -369,16 +369,16 @@ import java.util.List; selectedVariants.toArray(variants); HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat); - sampleStreamWrapper.prepare(); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; + sampleStreamWrapper.continuePreparing(); } // Build audio stream wrappers. for (int i = 0; i < audioVariants.size(); i++) { HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, baseUri, new HlsMasterPlaylist.HlsUrl[] {audioVariants.get(i)}, null, null); - sampleStreamWrapper.prepare(); sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; + sampleStreamWrapper.continuePreparing(); } // Build subtitle stream wrappers. diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 6c698d3c4d..fe756da0ef 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -144,8 +144,10 @@ import java.util.LinkedList; pendingResetPositionUs = positionUs; } - public void prepare() { - continueLoading(lastSeekPositionUs); + public void continuePreparing() { + if (!prepared) { + continueLoading(lastSeekPositionUs); + } } /** @@ -154,7 +156,8 @@ import java.util.LinkedList; */ public void prepareSingleTrack(Format format) { track(0).format(format); - endTracks(); + sampleQueuesBuilt = true; + maybeFinishPrepare(); } public void maybeThrowPrepareError() throws IOException { diff --git a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index 34ec76b673..b326c41b18 100644 --- a/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -413,11 +413,16 @@ public class DefaultHttpDataSource implements HttpDataSource { connection.setInstanceFollowRedirects(followRedirects); connection.setDoOutput(postBody != null); if (postBody != null) { - connection.setFixedLengthStreamingMode(postBody.length); - connection.connect(); - OutputStream os = connection.getOutputStream(); - os.write(postBody); - os.close(); + connection.setRequestMethod("POST"); + if (postBody.length == 0) { + connection.connect(); + } else { + connection.setFixedLengthStreamingMode(postBody.length); + connection.connect(); + OutputStream os = connection.getOutputStream(); + os.write(postBody); + os.close(); + } } else { connection.connect(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 561aba0146..4776e4d008 100644 --- a/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -65,6 +65,7 @@ public final class MimeTypes { public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm"; public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3"; public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + "/cea-608"; + public static final String APPLICATION_CEA708 = BASE_TYPE_APPLICATION + "/cea-708"; public static final String APPLICATION_SUBRIP = BASE_TYPE_APPLICATION + "/x-subrip"; public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml"; public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL"; diff --git a/playbacktests/src/main/AndroidManifest.xml b/playbacktests/src/main/AndroidManifest.xml index cd13b96b90..58ede793b2 100644 --- a/playbacktests/src/main/AndroidManifest.xml +++ b/playbacktests/src/main/AndroidManifest.xml @@ -17,8 +17,8 @@ + android:versionCode="2003" + android:versionName="2.0.3"> 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)