From 94c7ee72527d219a54e90281dab8c1d9cd994eb4 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 11 Oct 2016 11:27:27 -0700 Subject: [PATCH 01/10] Cronet - Skip if server doesn't support range requests ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=135819142 --- .../ext/cronet/CronetDataSourceTest.java | 133 ++++++++++-------- .../ext/cronet/CronetDataSource.java | 16 ++- 2 files changed, 86 insertions(+), 63 deletions(-) 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..15ffe5f141 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; + } } } From f18373eeb23b67e46cbc84e1467beba79eebd67b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 12 Oct 2016 06:18:06 -0700 Subject: [PATCH 02/10] Decouple TsExtractor's readers from TrackOutputs This allows the injectable reader factory to be a stateless factory, allows the seeking to be consistent and will allow multiple CC channel support later on. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=135909712 --- .../extractor/ts/AdtsReaderTest.java | 11 ++- .../extractor/ts/TsExtractorTest.java | 28 ++++--- .../exoplayer2/extractor/ts/Ac3Extractor.java | 4 +- .../exoplayer2/extractor/ts/Ac3Reader.java | 18 +++-- .../extractor/ts/AdtsExtractor.java | 4 +- .../exoplayer2/extractor/ts/AdtsReader.java | 36 ++++++--- .../ts/DefaultStreamReaderFactory.java | 78 ++++++------------- .../exoplayer2/extractor/ts/DtsReader.java | 21 +++-- .../extractor/ts/ElementaryStreamReader.java | 41 ++++++---- .../exoplayer2/extractor/ts/H262Reader.java | 11 ++- .../exoplayer2/extractor/ts/H264Reader.java | 35 +++++---- .../exoplayer2/extractor/ts/H265Reader.java | 19 +++-- .../exoplayer2/extractor/ts/Id3Reader.java | 15 +++- .../extractor/ts/MpegAudioReader.java | 15 +++- .../exoplayer2/extractor/ts/PsExtractor.java | 10 ++- .../exoplayer2/extractor/ts/TsExtractor.java | 61 +++++++++++---- .../exoplayer2/source/hls/HlsChunkSource.java | 32 ++++---- 17 files changed, 259 insertions(+), 180 deletions(-) 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..d1b5ce600d 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(true); + 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/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..f5a7d090e1 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,11 +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; @@ -68,6 +67,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 +81,18 @@ 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 ElementaryStreamReader id3Reader; public TsExtractor() { this(new TimestampAdjuster(0)); @@ -94,19 +102,23 @@ 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(); @@ -413,6 +425,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,17 +442,28 @@ 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)); } } @@ -447,7 +478,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 +510,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/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index caca5d9b83..6dade6a02f 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 @@ -369,29 +369,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. From 64262085a71f822741b65d78c0e883abaa15d8c1 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 12 Oct 2016 08:48:37 -0700 Subject: [PATCH 03/10] Block when surface being replaced is non-null A blocking call is necessary where we want to guarantee that the player wont access the surface after the method call has returned. We currently only do this for the case: Surface->Null But we should also do it for the case: SurfaceA->SurfaceB Since the caller may reasonably do something like destroy SurfaceA immediately after it's been replaced. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=135921296 --- .../com/google/android/exoplayer2/SimpleExoPlayer.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 f2c26b9495..e621e19a48 100644 --- a/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -619,7 +619,8 @@ public final class SimpleExoPlayer implements ExoPlayer { } private void setVideoSurfaceInternal(Surface surface) { - this.surface = surface; + // 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) { @@ -627,12 +628,13 @@ 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) { + // 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; } private final class ComponentListener implements VideoRendererEventListener, From ff712aead598515a6c9d47bf3050f185c226774c Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 13 Oct 2016 04:40:44 -0700 Subject: [PATCH 04/10] Try not adapting before failing with BehindLiveWindowException in Hls Issue:#1782 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136025847 --- .../exoplayer2/source/hls/HlsChunkSource.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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 6dade6a02f..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 { From e685edc179837f658bd551992c720dd8dc7bff9f Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 13 Oct 2016 04:54:13 -0700 Subject: [PATCH 05/10] Make interface implementation consistent among ExtractorOutputs The method track(int id) currently has different behaviours across implementations. This CL maps ids to track outputs, which means that successive calls with the same id will return the same TrackOutput instance. Also fixes TsExtractor inconsistent behavior after a seek. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136026721 --- .../extractor/ts/AdtsReaderTest.java | 2 +- .../exoplayer2/extractor/Extractor.java | 2 +- .../exoplayer2/extractor/ExtractorOutput.java | 13 ++-- .../exoplayer2/extractor/ts/TsExtractor.java | 34 ++++++--- .../source/ExtractorMediaPeriod.java | 74 +++++++++++-------- .../source/chunk/ChunkExtractorWrapper.java | 4 +- .../testutil/FakeExtractorOutput.java | 11 +-- .../android/exoplayer2/testutil/TestUtil.java | 4 +- 8 files changed, 82 insertions(+), 62 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java index d1b5ce600d..e19de76466 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java @@ -68,7 +68,7 @@ public class AdtsReaderTest extends TestCase { @Override protected void setUp() throws Exception { - FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(true); + FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); adtsOutput = fakeExtractorOutput.track(0); id3Output = fakeExtractorOutput.track(1); adtsReader = new AdtsReader(true); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java index 6bf65710fc..4120110afb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java @@ -56,7 +56,7 @@ public interface Extractor { boolean sniff(ExtractorInput input) throws IOException, InterruptedException; /** - * Initializes the extractor with an {@link ExtractorOutput}. + * Initializes the extractor with an {@link ExtractorOutput}. Called at most once. * * @param output An {@link ExtractorOutput} to receive extracted data. */ diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java index d138c7ce3a..a547f745ca 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ExtractorOutput.java @@ -21,18 +21,19 @@ package com.google.android.exoplayer2.extractor; public interface ExtractorOutput { /** - * Called when the {@link Extractor} identifies the existence of a track in the stream. + * Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track. *

- * Returns a {@link TrackOutput} that will receive track level data belonging to the track. + * The same {@link TrackOutput} is returned if multiple calls are made with the same + * {@code trackId}. * - * @param trackId A unique track identifier. - * @return The {@link TrackOutput} that should receive track level data belonging to the track. + * @param trackId A track identifier. + * @return The {@link TrackOutput} for the given track identifier. */ TrackOutput track(int trackId); /** - * Called when all tracks have been identified, meaning that {@link #track(int)} will not be - * called again. + * Called when all tracks have been identified, meaning no new {@code trackId} values will be + * passed to {@link #track(int)}. */ void endTracks(); diff --git a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index f5a7d090e1..bac362d711 100644 --- a/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -54,7 +54,6 @@ public final class TsExtractor implements Extractor { }; - public static final int TS_STREAM_TYPE_MPA = 0x03; public static final int TS_STREAM_TYPE_MPA_LSF = 0x04; public static final int TS_STREAM_TYPE_AAC = 0x0F; @@ -92,6 +91,7 @@ public final class TsExtractor implements Extractor { // Accessed only by the loading thread. private ExtractorOutput output; + private boolean tracksEnded; private ElementaryStreamReader id3Reader; public TsExtractor() { @@ -120,8 +120,8 @@ public final class TsExtractor implements Extractor { tsScratch = new ParsableBitArray(new byte[3]); trackIds = new SparseBooleanArray(); tsPayloadReaders = new SparseArray<>(); - tsPayloadReaders.put(TS_PAT_PID, new PatReader()); continuityCounters = new SparseIntArray(); + resetPayloadReaders(); } // Extractor implementation. @@ -153,11 +153,10 @@ public final class TsExtractor implements Extractor { @Override public void seek(long position) { timestampAdjuster.reset(); - for (int i = 0; i < tsPayloadReaders.size(); i++) { - tsPayloadReaders.valueAt(i).seek(); - } tsPacketBuffer.reset(); continuityCounters.clear(); + // Elementary stream readers' state should be cleared to get consistent behaviours when seeking. + resetPayloadReaders(); } @Override @@ -252,6 +251,13 @@ public final class TsExtractor implements Extractor { // Internals. + private void resetPayloadReaders() { + trackIds.clear(); + tsPayloadReaders.clear(); + tsPayloadReaders.put(TS_PAT_PID, new PatReader()); + id3Reader = null; + } + /** * Parses TS packet payload data. */ @@ -345,7 +351,7 @@ public final class TsExtractor implements Extractor { patScratch.skipBits(13); // network_PID (13) } else { int pid = patScratch.readBits(13); - tsPayloadReaders.put(pid, new PmtReader()); + tsPayloadReaders.put(pid, new PmtReader(pid)); } } } @@ -365,14 +371,16 @@ public final class TsExtractor implements Extractor { private final ParsableBitArray pmtScratch; private final ParsableByteArray sectionData; + private final int pid; private int sectionLength; private int sectionBytesRead; private int crc; - public PmtReader() { + public PmtReader(int pid) { pmtScratch = new ParsableBitArray(new byte[5]); sectionData = new ParsableByteArray(); + this.pid = pid; } @Override @@ -466,8 +474,16 @@ public final class TsExtractor implements Extractor { tsPayloadReaders.put(elementaryPid, new PesReader(pesPayloadReader, timestampAdjuster)); } } - - output.endTracks(); + if (mapByType) { + if (!tracksEnded) { + output.endTracks(); + } + } else { + tsPayloadReaders.remove(TS_PAT_PID); + tsPayloadReaders.remove(pid); + output.endTracks(); + } + tracksEnded = true; } /** diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 49f2cffca5..27bd1f677f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; +import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; @@ -41,7 +42,6 @@ import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; -import java.util.Arrays; /** * A {@link MediaPeriod} that extracts data using an {@link Extractor}. @@ -68,6 +68,7 @@ import java.util.Arrays; private final Runnable maybeFinishPrepareRunnable; private final Runnable onContinueLoadingRequestedRunnable; private final Handler handler; + private final SparseArray sampleQueues; private Callback callback; private SeekMap seekMap; @@ -77,7 +78,6 @@ import java.util.Arrays; private boolean seenFirstTrackSelection; private boolean notifyReset; private int enabledTrackCount; - private DefaultTrackOutput[] sampleQueues; private TrackGroupArray tracks; private long durationUs; private boolean[] trackEnabledStates; @@ -131,7 +131,7 @@ import java.util.Arrays; handler = new Handler(); pendingResetPositionUs = C.TIME_UNSET; - sampleQueues = new DefaultTrackOutput[0]; + sampleQueues = new SparseArray<>(); length = C.LENGTH_UNSET; } @@ -141,8 +141,9 @@ import java.util.Arrays; @Override public void run() { extractorHolder.release(); - for (DefaultTrackOutput sampleQueue : sampleQueues) { - sampleQueue.disable(); + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { + sampleQueues.valueAt(i).disable(); } } }); @@ -178,7 +179,7 @@ import java.util.Arrays; Assertions.checkState(trackEnabledStates[track]); enabledTrackCount--; trackEnabledStates[track] = false; - sampleQueues[track].disable(); + sampleQueues.valueAt(track).disable(); streams[i] = null; } } @@ -201,9 +202,10 @@ import java.util.Arrays; if (!seenFirstTrackSelection) { // At the time of the first track selection all queues will be enabled, so we need to disable // any that are no longer required. - for (int i = 0; i < sampleQueues.length; i++) { + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { if (!trackEnabledStates[i]) { - sampleQueues[i].disable(); + sampleQueues.valueAt(i).disable(); } } } @@ -270,11 +272,12 @@ import java.util.Arrays; // Treat all seeks into non-seekable media as being to t=0. positionUs = seekMap.isSeekable() ? positionUs : 0; lastSeekPositionUs = positionUs; + int trackCount = sampleQueues.size(); // If we're not pending a reset, see if we can seek within the sample queues. boolean seekInsideBuffer = !isPendingReset(); - for (int i = 0; seekInsideBuffer && i < sampleQueues.length; i++) { + for (int i = 0; seekInsideBuffer && i < trackCount; i++) { if (trackEnabledStates[i]) { - seekInsideBuffer = sampleQueues[i].skipToKeyframeBefore(positionUs); + seekInsideBuffer = sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs); } } // If we failed to seek within the sample queues, we need to restart. @@ -284,8 +287,8 @@ import java.util.Arrays; if (loader.isLoading()) { loader.cancelLoading(); } else { - for (int i = 0; i < sampleQueues.length; i++) { - sampleQueues[i].reset(trackEnabledStates[i]); + for (int i = 0; i < trackCount; i++) { + sampleQueues.valueAt(i).reset(trackEnabledStates[i]); } } } @@ -296,7 +299,7 @@ import java.util.Arrays; // SampleStream methods. /* package */ boolean isReady(int track) { - return loadingFinished || (!isPendingReset() && !sampleQueues[track].isEmpty()); + return loadingFinished || (!isPendingReset() && !sampleQueues.valueAt(track).isEmpty()); } /* package */ void maybeThrowError() throws IOException { @@ -308,7 +311,8 @@ import java.util.Arrays; return C.RESULT_NOTHING_READ; } - return sampleQueues[track].readData(formatHolder, buffer, loadingFinished, lastSeekPositionUs); + return sampleQueues.valueAt(track).readData(formatHolder, buffer, loadingFinished, + lastSeekPositionUs); } // Loader.Callback implementation. @@ -332,8 +336,9 @@ import java.util.Arrays; long loadDurationMs, boolean released) { copyLengthFromLoader(loadable); if (!released && enabledTrackCount > 0) { - for (int i = 0; i < sampleQueues.length; i++) { - sampleQueues[i].reset(trackEnabledStates[i]); + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { + sampleQueues.valueAt(i).reset(trackEnabledStates[i]); } callback.onContinueLoadingRequested(this); } @@ -358,11 +363,13 @@ import java.util.Arrays; @Override public TrackOutput track(int id) { - sampleQueues = Arrays.copyOf(sampleQueues, sampleQueues.length + 1); - DefaultTrackOutput sampleQueue = new DefaultTrackOutput(allocator); - sampleQueue.setUpstreamFormatChangeListener(this); - sampleQueues[sampleQueues.length - 1] = sampleQueue; - return sampleQueue; + DefaultTrackOutput trackOutput = sampleQueues.get(id); + if (trackOutput == null) { + trackOutput = new DefaultTrackOutput(allocator); + trackOutput.setUpstreamFormatChangeListener(this); + sampleQueues.put(id, trackOutput); + } + return trackOutput; } @Override @@ -390,18 +397,18 @@ import java.util.Arrays; if (released || prepared || seekMap == null || !tracksBuilt) { return; } - for (DefaultTrackOutput sampleQueue : sampleQueues) { - if (sampleQueue.getUpstreamFormat() == null) { + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { + if (sampleQueues.valueAt(i).getUpstreamFormat() == null) { return; } } loadCondition.close(); - int trackCount = sampleQueues.length; TrackGroup[] trackArray = new TrackGroup[trackCount]; trackEnabledStates = new boolean[trackCount]; durationUs = seekMap.getDurationUs(); for (int i = 0; i < trackCount; i++) { - trackArray[i] = new TrackGroup(sampleQueues[i].getUpstreamFormat()); + trackArray[i] = new TrackGroup(sampleQueues.valueAt(i).getUpstreamFormat()); } tracks = new TrackGroupArray(trackArray); prepared = true; @@ -455,8 +462,9 @@ import java.util.Arrays; // a new load. lastSeekPositionUs = 0; notifyReset = prepared; - for (int i = 0; i < sampleQueues.length; i++) { - sampleQueues[i].reset(!prepared || trackEnabledStates[i]); + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { + sampleQueues.valueAt(i).reset(!prepared || trackEnabledStates[i]); } loadable.setLoadPosition(0); } @@ -464,17 +472,19 @@ import java.util.Arrays; private int getExtractedSamplesCount() { int extractedSamplesCount = 0; - for (DefaultTrackOutput sampleQueue : sampleQueues) { - extractedSamplesCount += sampleQueue.getWriteIndex(); + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { + extractedSamplesCount += sampleQueues.valueAt(i).getWriteIndex(); } return extractedSamplesCount; } private long getLargestQueuedTimestampUs() { long largestQueuedTimestampUs = Long.MIN_VALUE; - for (DefaultTrackOutput sampleQueue : sampleQueues) { + int trackCount = sampleQueues.size(); + for (int i = 0; i < trackCount; i++) { largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, - sampleQueue.getLargestQueuedTimestampUs()); + sampleQueues.valueAt(i).getLargestQueuedTimestampUs()); } return largestQueuedTimestampUs; } @@ -523,7 +533,7 @@ import java.util.Arrays; @Override public void skipToKeyframeBefore(long timeUs) { - sampleQueues[track].skipToKeyframeBefore(timeUs); + sampleQueues.valueAt(track).skipToKeyframeBefore(timeUs); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java index e316215160..b9aa098b9d 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -59,6 +59,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput // Accessed only on the loader thread. private boolean seenTrack; + private int seenTrackId; /** * @param extractor The extractor to wrap. @@ -116,8 +117,9 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput @Override public TrackOutput track(int id) { - Assertions.checkState(!seenTrack); + Assertions.checkState(!seenTrack || seenTrackId == id); seenTrack = true; + seenTrackId = id; return this; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java index b0ab90789c..3716c6d37f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java @@ -23,7 +23,6 @@ import java.io.File; import java.io.IOException; import java.io.PrintWriter; import junit.framework.Assert; -import junit.framework.TestCase; /** * A fake {@link ExtractorOutput}. @@ -37,8 +36,6 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab */ private static final boolean WRITE_DUMP = false; - private final boolean allowDuplicateTrackIds; - public final SparseArray trackOutputs; public int numberOfTracks; @@ -46,11 +43,6 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab public SeekMap seekMap; public FakeExtractorOutput() { - this(false); - } - - public FakeExtractorOutput(boolean allowDuplicateTrackIds) { - this.allowDuplicateTrackIds = allowDuplicateTrackIds; trackOutputs = new SparseArray<>(); } @@ -58,11 +50,10 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab public FakeTrackOutput track(int trackId) { FakeTrackOutput output = trackOutputs.get(trackId); if (output == null) { + Assert.assertFalse(tracksEnded); numberOfTracks++; output = new FakeTrackOutput(); trackOutputs.put(trackId, output); - } else { - TestCase.assertTrue("Duplicate track id: " + trackId, allowDuplicateTrackIds); } return output; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index 7b88062718..6f4578b694 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -267,8 +267,8 @@ public class TestUtil { */ public static FakeExtractorOutput assertOutput(Extractor extractor, String sampleFile, byte[] fileData, Instrumentation instrumentation, boolean simulateIOErrors, - boolean simulateUnknownLength, - boolean simulatePartialReads) throws IOException, InterruptedException { + boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException, + InterruptedException { FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData) .setSimulateIOErrors(simulateIOErrors) .setSimulateUnknownLength(simulateUnknownLength) From a22390c29b253694dd4db326bd5bb77c06b7e703 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 13 Oct 2016 07:35:32 -0700 Subject: [PATCH 06/10] Parse CEA-708 codec for rawCC Note that actually handling CEA-708 is not yet implemented, and so this is a no-op change from a behavior point of view. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136038439 --- .../exoplayer2/source/dash/manifest/DashManifestParser.java | 4 +++- .../java/com/google/android/exoplayer2/util/MimeTypes.java | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) 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/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"; From 6acf59c4fc12bfca9c77bdea3f69cf3b33d66d45 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 13 Oct 2016 08:23:39 -0700 Subject: [PATCH 07/10] Fix Widevine L3 provisioning in V2 1. HttpMediaDrmCallback.executeProvisionRequest needs to specify an empty byte[], else we do a GET instead of a POST. 2. Content-Type should not be set when making the provision request, since there's no body. 3. DataSource implementations must correctly handle a non-null body with zero length. CronetDataSource was not handling this case. DefaultHttpDataSource was, but made a code modification to make it a little clearer. OkHttpDataSource seems to handle the case correctly, and it doens't look like the code can be made clearer. Issue #1925 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136042641 --- .../exoplayer2/ext/cronet/CronetDataSource.java | 15 ++++++++++----- .../exoplayer2/drm/HttpMediaDrmCallback.java | 5 ++--- .../upstream/DefaultHttpDataSource.java | 15 ++++++++++----- 3 files changed, 22 insertions(+), 13 deletions(-) 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 15ffe5f141..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 @@ -416,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()); @@ -434,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/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/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(); } From dca4d16bef3526168a8c24db14ffcbd66ff9f0c4 Mon Sep 17 00:00:00 2001 From: klampert Date: Fri, 14 Oct 2016 09:15:13 -0700 Subject: [PATCH 08/10] Release surfaces created to wrap SurfaceTextures ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136163292 --- .../android/exoplayer2/SimpleExoPlayer.java | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) 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 e621e19a48..4b673d3750 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); } } @@ -468,6 +469,12 @@ public final class SimpleExoPlayer implements ExoPlayer { public void release() { player.release(); removeSurfaceCallbacks(); + if (surface != null) { + if (ownsSurface) { + surface.release(); + } + surface = null; + } } @Override @@ -618,7 +625,7 @@ public final class SimpleExoPlayer implements ExoPlayer { } } - private void setVideoSurfaceInternal(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]; @@ -629,12 +636,17 @@ public final class SimpleExoPlayer implements ExoPlayer { } } 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, @@ -783,7 +795,7 @@ public final class SimpleExoPlayer implements ExoPlayer { @Override public void surfaceCreated(SurfaceHolder holder) { - setVideoSurfaceInternal(holder.getSurface()); + setVideoSurfaceInternal(holder.getSurface(), false); } @Override @@ -793,14 +805,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 @@ -810,7 +822,7 @@ public final class SimpleExoPlayer implements ExoPlayer { @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { - setVideoSurface(null); + setVideoSurfaceInternal(null, true); return true; } From e873b4b6abf4827b04fcf7074c7c00cd3e5073c4 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 14 Oct 2016 11:16:16 -0700 Subject: [PATCH 09/10] Change prepare() for maybePrepare() in HlsSampleStreamWrapper This will allow asynchronous preparation. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136176854 --- .../android/exoplayer2/source/hls/HlsMediaPeriod.java | 6 +++--- .../exoplayer2/source/hls/HlsSampleStreamWrapper.java | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) 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 { From cecb1f5f765bc834a307d9f65315c880bf7844cd Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 17 Oct 2016 05:03:25 -0700 Subject: [PATCH 10/10] Bump version + update release notes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=136339035 --- RELEASENOTES.md | 27 +++++++++++++++++++ build.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 +-- .../exoplayer2/ExoPlayerLibraryInfo.java | 4 +-- playbacktests/src/main/AndroidManifest.xml | 4 +-- 5 files changed, 34 insertions(+), 7 deletions(-) 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/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/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">